From c92670e759b8795039385f1fbfd85a91bc8ec708 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Fri, 3 Apr 2026 15:14:33 +0200 Subject: [PATCH 1/8] refactor of logic code --- .github/workflows/golangci-lint.yml | 7 +- .golangci.yml | 11 + api/cashu/auth.go | 2 - api/cashu/errors.go | 47 +- api/cashu/erros_test.go | 6 +- api/cashu/keys.go | 5 +- api/cashu/melt.go | 4 +- api/cashu/melt_test.go | 2 - api/cashu/proofs.go | 5 - api/cashu/proofs_test.go | 5 - api/cashu/spend_condition.go | 16 +- api/cashu/spend_condition_test.go | 11 - api/cashu/swap.go | 5 +- api/cashu/swap_test.go | 2 - api/cashu/types.go | 19 +- api/cashu/types_test.go | 3 - api/cashu/util.go | 8 - api/cashu/util_test.go | 3 - cmd/nutmix/admin_test.go | 1 - cmd/nutmix/htlc_route_test.go | 14 +- cmd/nutmix/main.go | 5 +- cmd/nutmix/main_test.go | 183 +++-- cmd/nutmix/p2pk_route_test.go | 10 +- cmd/nutmix/payment_error_handling_test.go | 126 +--- internal/database/goose/goose.go | 7 +- internal/database/mock_db/auth.go | 5 - internal/database/mock_db/change.go | 8 - internal/database/postgresql/auth.go | 4 - internal/database/postgresql/change.go | 3 - internal/database/postgresql/main.go | 33 +- .../database/postgresql/operations_test.go | 11 +- internal/lightning/backend.go | 2 +- internal/lightning/cln.go | 26 +- internal/lightning/fake_wallet.go | 12 +- internal/lightning/invoice.go | 4 - internal/lightning/lightning_test.go | 2 - internal/lightning/lnbits.go | 15 +- internal/lightning/lnd.go | 17 +- internal/lightning/strike.go | 13 +- internal/mint/auth.go | 9 +- internal/mint/bolt11.go | 9 +- internal/mint/info.go | 193 ++++++ internal/mint/interface.go | 9 + internal/mint/melting.go | 511 ++++++++++---- internal/mint/mint.go | 8 +- internal/mint/mint_test.go | 8 +- internal/mint/minting.go | 392 +++++++++++ internal/mint/proofs.go | 17 +- internal/mint/restore.go | 40 ++ internal/mint/seeds.go | 3 - internal/mint/swapping.go | 228 +++++++ internal/mint/transaction_persistence_test.go | 628 ++++++++++++++++++ internal/mint/utils.go | 90 +-- internal/mint/utils_test.go | 22 +- internal/routes/admin/auth.go | 9 +- internal/routes/admin/crons.go | 22 +- internal/routes/admin/keysets.go | 9 +- internal/routes/admin/lightning.go | 1 - internal/routes/admin/liquidity-manager.go | 61 +- internal/routes/admin/main.go | 2 - internal/routes/admin/tabs_test.go | 1 - internal/routes/auth.go | 45 +- internal/routes/bolt11.go | 546 +-------------- internal/routes/middleware/auth_test.go | 2 - internal/routes/middleware/cache.go | 4 +- internal/routes/mint.go | 412 +----------- internal/routes/routes.go | 1 - internal/routes/websocket.go | 10 +- internal/signer/local_signer/derivation.go | 12 +- internal/signer/local_signer/legacy.go | 6 +- .../signer/local_signer/local_signer_test.go | 5 - internal/signer/local_signer/signatory.go | 1 - internal/signer/local_signer/signer.go | 25 +- .../signer/remote_signer/remote_signer.go | 15 +- internal/signer/remote_signer/transformer.go | 9 +- internal/signer/remote_signer/util.go | 2 - internal/signer/utils.go | 2 - internal/utils/common.go | 3 - internal/utils/files.go | 17 +- internal/utils/liquidityManager.go | 2 - internal/utils/proofs.go | 40 +- internal/utils/proofs_test.go | 18 +- internal/utils/testing.go | 5 +- pkg/crypto/bdhke.go | 2 - pkg/crypto/bdhke_test.go | 2 - test/configTest/setup.go | 4 - test/setupTest/lnd_test.go | 8 +- 87 files changed, 2344 insertions(+), 1788 deletions(-) create mode 100644 internal/mint/info.go create mode 100644 internal/mint/interface.go create mode 100644 internal/mint/minting.go create mode 100644 internal/mint/restore.go create mode 100644 internal/mint/swapping.go create mode 100644 internal/mint/transaction_persistence_test.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 26caf0cf..012d7ea6 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -29,5 +29,10 @@ jobs: just gen-templ just web-install just web-build-prod - - name: golangci-lint + - name: Install golangci-lint uses: golangci/golangci-lint-action@v9 + with: + version: v2.6.1 + install-only: true + - name: Run lint + run: just lint diff --git a/.golangci.yml b/.golangci.yml index 2f852c2b..38884ca1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,6 +12,8 @@ linters: - bidichk # Checks for dangerous unicode character sequences - bodyclose # Checks HTTP response body is closed - contextcheck # Check for non-inherited context + - musttag # enforces field tags in (un)marshaled structs + - staticcheck # is a go vet on steroids, applying a ton of static analysis checks # Bug detection - staticcheck # Comprehensive static analysis @@ -26,6 +28,15 @@ linters: - unparam # Unused function parameters - misspell # Commonly misspelled words - exhaustruct # [highly recommend to enable] checks if all structure fields are initialized + - iface # checks the incorrect use of interfaces, helping developers avoid interface pollution + - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed + - sloglint # ensure consistent code style when using log/slog + - unconvert # removes unnecessary type conversions + - unparam # reports unused function parameters + - unqueryvet # detects SELECT * in SQL queries and SQL builders, encouraging explicit column selection + - whitespace # detects leading and trailing whitespace + # - noinlineerr # disallows inline error handling `if err := ...; err != nil {` + - prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated settings: gosec: diff --git a/api/cashu/auth.go b/api/cashu/auth.go index 3fc90529..1416358a 100644 --- a/api/cashu/auth.go +++ b/api/cashu/auth.go @@ -80,7 +80,6 @@ type Nut22Info struct { } func ConvertRouteListToProtectedRouteList(list []string) []ProtectedRoute { - routes := []ProtectedRoute{} for _, v := range list { @@ -92,7 +91,6 @@ func ConvertRouteListToProtectedRouteList(list []string) []ProtectedRoute { Path: v, }, ) - } return routes } diff --git a/api/cashu/errors.go b/api/cashu/errors.go index 0b4bf0be..cfad6068 100644 --- a/api/cashu/errors.go +++ b/api/cashu/errors.go @@ -11,9 +11,20 @@ var ( ErrDifferentInputOutputUnit = errors.New("different input output unit") ErrNotEnoughtProofs = errors.New("not enough proofs") ErrProofSpent = errors.New("proof already spent") + ErrProofPending = errors.New("Proofs are pending") ErrBlindMessageAlreadySigned = errors.New("blind message already signed") ErrCommonSecretNotCorrectSize = errors.New("proof secret is not correct size") ErrUnknown = errors.New("unknown error") + ErrPaymentMethodNotSupported = errors.New("payment method not supported") + + ErrMintRequestAlreadyIssued = errors.New("mint request already issued") + ErrAmountNotEqualToInvoice = errors.New("Amount in request does not equal invoice") + + ErrMintintDisabled = errors.New("minting is disabled") + ErrAmountOutsideLimit = errors.New("amount is outside the limit") + ErrRequestNotPaid = errors.New("request not paid yet") + + ErrAmountlessInvoiceNotSupported = errors.New("Amount less invoices not supported") ) type ErrorCode uint @@ -21,17 +32,19 @@ type ErrorCode uint const ( PROOF_VERIFICATION_FAILED ErrorCode = 10001 - PROOF_ALREADY_SPENT ErrorCode = 11001 - PROOFS_PENDING ErrorCode = 11002 - OUTPUTS_ALREADY_SIGNED ErrorCode = 11003 - OUTPUTS_PENDING ErrorCode = 11004 - TRANSACTION_NOT_BALANCED ErrorCode = 11005 - INSUFICIENT_FEE ErrorCode = 11006 - DUPLICATE_INPUTS ErrorCode = 11007 - DUPLICATE_OUTPUTS ErrorCode = 11008 - MULTIPLE_UNITS_OUTPUT_INPUT ErrorCode = 11009 - INPUT_OUTPUT_NOT_SAME_UNIT ErrorCode = 11010 - UNIT_NOT_SUPPORTED ErrorCode = 11013 + PROOF_ALREADY_SPENT ErrorCode = 11001 + PROOFS_PENDING ErrorCode = 11002 + OUTPUTS_ALREADY_SIGNED ErrorCode = 11003 + OUTPUTS_PENDING ErrorCode = 11004 + TRANSACTION_NOT_BALANCED ErrorCode = 11005 + INSUFICIENT_OUTSIDE_LIMIT ErrorCode = 11006 + DUPLICATE_INPUTS ErrorCode = 11007 + DUPLICATE_OUTPUTS ErrorCode = 11008 + MULTIPLE_UNITS_OUTPUT_INPUT ErrorCode = 11009 + INPUT_OUTPUT_NOT_SAME_UNIT ErrorCode = 11010 + AMOUNT_LESS_INVOICE_NOT_SUPPORTED ErrorCode = 11011 + AMOUNT_NOT_EQUAL_TO_INVOICE ErrorCode = 11012 + UNIT_NOT_SUPPORTED ErrorCode = 11013 KEYSET_NOT_KNOW ErrorCode = 12001 INACTIVE_KEYSET ErrorCode = 12002 @@ -58,7 +71,6 @@ const ( ) func (e ErrorCode) String() string { - error := "" switch e { case OUTPUTS_ALREADY_SIGNED: @@ -76,8 +88,8 @@ func (e ErrorCode) String() string { error = "Transaction is not balanced (inputs != outputs)" case UNIT_NOT_SUPPORTED: error = "Unit in request is not supported" - case INSUFICIENT_FEE: - error = "Insufficient fee" + case INSUFICIENT_OUTSIDE_LIMIT: + error = "Amount outside limit" case DUPLICATE_INPUTS: error = "Duplicate inputs provided" case DUPLICATE_OUTPUTS: @@ -86,6 +98,10 @@ func (e ErrorCode) String() string { error = "Inputs/Outputs of multiple units" case INPUT_OUTPUT_NOT_SAME_UNIT: error = "Inputs and outputs are not same unit" + case AMOUNT_NOT_EQUAL_TO_INVOICE: + error = "Amount in request does not equal invoice" + case AMOUNT_LESS_INVOICE_NOT_SUPPORTED: + error = "Amountless invoices are not supported" case KEYSET_NOT_KNOW: error = "Keyset is not known" @@ -120,6 +136,8 @@ func (e ErrorCode) String() string { error = "Maximum Blind auth token amounts execeeded" case MAXIMUM_BAT_RATE_LIMIT_EXCEEDED: error = "Maximum BAT rate limit execeeded" + case UNKNOWN: + error = "Unknown error" } return error @@ -132,7 +150,6 @@ type ErrorResponse struct { } func ErrorCodeToResponse(code ErrorCode, detail *string) ErrorResponse { - return ErrorResponse{ Code: code, Error: code.String(), diff --git a/api/cashu/erros_test.go b/api/cashu/erros_test.go index 18a0f369..4b4d045f 100644 --- a/api/cashu/erros_test.go +++ b/api/cashu/erros_test.go @@ -3,15 +3,13 @@ package cashu import "testing" func TestCreatingAnErrorResponse(t *testing.T) { - - response := ErrorCodeToResponse(INSUFICIENT_FEE, nil) + response := ErrorCodeToResponse(INSUFICIENT_OUTSIDE_LIMIT, nil) if response.Code != 11006 { t.Errorf("Did not get the correct error node.") } - if response.Error != "Insufficient fee" { + if response.Error != "Amount outside limit" { t.Errorf("Incorrect error string") } - } diff --git a/api/cashu/keys.go b/api/cashu/keys.go index b46c749f..85b9c898 100644 --- a/api/cashu/keys.go +++ b/api/cashu/keys.go @@ -51,7 +51,6 @@ func sortPubkeyMapToOrganizedArray(pubkeyMap map[uint64]*secp256k1.PublicKey) [] return int(a.Amount) - int(b.Amount) }) return arrayPubkeys - } func generateKeysetV2Preimage(sortedPubkeyArray []pubkeyWithAmount, unit string, fee uint, finalExpiry *time.Time) string { @@ -83,7 +82,7 @@ func DeriveKeysetIdV2(pubKeysMap map[uint64]*secp256k1.PublicKey, unit string, f } func GenerateKeysets(versionKey *bip32.Key, values []uint64, seed Seed) ([]MintKey, error) { - var keysets []MintKey + var keysets = make([]MintKey, len(values)) // Get the current time currentTime := time.Now() @@ -109,7 +108,7 @@ func GenerateKeysets(versionKey *bip32.Key, values []uint64, seed Seed) ([]MintK FinalExpiry: seed.FinalExpiry, } - keysets = append(keysets, keyset) + keysets[i] = keyset } return keysets, nil diff --git a/api/cashu/melt.go b/api/cashu/melt.go index b1384e75..b6c16ce5 100644 --- a/api/cashu/melt.go +++ b/api/cashu/melt.go @@ -42,7 +42,6 @@ func (meltRequest *MeltRequestDB) GetPostMeltQuoteResponse() PostMeltQuoteBolt11 Unit: meltRequest.Unit, Change: []BlindSignature{}, } - } type PostMeltQuoteBolt11Options struct { @@ -77,7 +76,7 @@ type PostMeltQuoteBolt11Response struct { type PostMeltBolt11Request struct { Quote string `json:"quote"` Inputs Proofs `json:"inputs"` - Outputs []BlindedMessage `json:"outputs"` + Outputs BlindedMessages `json:"outputs"` } func (p *PostMeltBolt11Request) ValidateSigflag() error { @@ -156,7 +155,6 @@ func (p *PostMeltBolt11Request) verifyConditions() error { if spendCondition.Data.Tags.originalTag != firstSpendCondition.Data.Tags.originalTag { return fmt.Errorf("not same tags %w", ErrInvalidSpendCondition) } - } return nil } diff --git a/api/cashu/melt_test.go b/api/cashu/melt_test.go index 10430eec..76f9ffb3 100644 --- a/api/cashu/melt_test.go +++ b/api/cashu/melt_test.go @@ -44,7 +44,6 @@ func TestMeltRequestMsg(t *testing.T) { if hex.EncodeToString(hashMessage[:]) != "9efa1067cc7dc870f4074f695115829c3cd817a6866c3b84e9814adf3c3cf262" { t.Errorf("hash message is wrong %v", msg) } - } func TestMeltRequestValidSignature(t *testing.T) { @@ -110,6 +109,5 @@ func TestMeltRequestValidMultiSig(t *testing.T) { err = meltRequest.ValidateSigflag() if err != nil { t.Errorf("there should not have been any error on multisig! %+v ", err) - } } diff --git a/api/cashu/proofs.go b/api/cashu/proofs.go index 462cabc9..79642748 100644 --- a/api/cashu/proofs.go +++ b/api/cashu/proofs.go @@ -326,13 +326,11 @@ func (p Proof) IsProofSpendConditioned() (bool, *SpendCondition, error) { spendCondition, err := p.parseSpendCondition() if err != nil { return false, nil, fmt.Errorf("p.parseSpendCondition(). %w", err) - } return true, spendCondition, nil } func (p Proof) HashSecretToCurve() (Proof, error) { - // Get Hash to curve of secret parsedProof := []byte(p.Secret) @@ -379,7 +377,6 @@ func (p *Proof) Sign(privkey *secp256k1.PrivateKey) error { return nil } func (p *Proof) AddPreimage(preimage string) error { - var witness Witness if p.Witness == "" { witness = Witness{ @@ -446,7 +443,6 @@ func VerifyProofsSpendConditions(proofs Proofs) error { return fmt.Errorf("proof.IsProofSpendConditioned(). %+v", err) } if isLocked { - err = spendCondition.CheckValid() if err != nil { return fmt.Errorf("spendCondition.CheckValid(). %w", err) @@ -469,7 +465,6 @@ func VerifyProofsSpendConditions(proofs Proofs) error { return ErrInvalidSpendCondition } } - } if !isLocked { if len(proof.Secret) != 64 { diff --git a/api/cashu/proofs_test.go b/api/cashu/proofs_test.go index 4d06b592..e845a1ff 100644 --- a/api/cashu/proofs_test.go +++ b/api/cashu/proofs_test.go @@ -106,13 +106,10 @@ func TestCheckP2PKProof(t *testing.T) { valid, err := proof.VerifyP2PK(spendCondition) if err != nil { t.Errorf("should not have errored. %+v", err) - } if !valid { - t.Errorf("proof should have been valid") } - } func TestCheckP2PKProofInvalidSignature(t *testing.T) { @@ -164,7 +161,6 @@ func TestCheckP2PKProofValidMultisig2of2(t *testing.T) { valid, err := proof.VerifyP2PK(spendCondition) if err != nil { t.Errorf("should not have errored. %+v", err) - } if !valid { t.Errorf("proof should have been valid") @@ -219,7 +215,6 @@ func TestCheckP2PKProofWithSpendableLocktime(t *testing.T) { valid, err := proof.VerifyP2PK(spendCondition) if err != nil { t.Errorf("should not have errored. %+v", err) - } if !valid { t.Errorf("proof should have been valid") diff --git a/api/cashu/spend_condition.go b/api/cashu/spend_condition.go index 1191438a..d2da2002 100644 --- a/api/cashu/spend_condition.go +++ b/api/cashu/spend_condition.go @@ -182,7 +182,6 @@ func (tags *TagsInfo) MarshalJSON() ([]byte, error) { } func (tags *TagsInfo) UnmarshalJSON(b []byte) error { - var arrayToCheck [][]string err := json.Unmarshal(b, &arrayToCheck) @@ -192,7 +191,6 @@ func (tags *TagsInfo) UnmarshalJSON(b []byte) error { } for _, tag := range arrayToCheck { - if len(tag) < 2 { return fmt.Errorf("%w: %s", ErrMalformedTag, tag) } @@ -205,7 +203,6 @@ func (tags *TagsInfo) UnmarshalJSON(b []byte) error { tagInfo := tag[1:] switch tagName { - case Sigflag: if len(tagInfo) != 1 { return fmt.Errorf("%w: %s", ErrMalformedTag, tag) @@ -241,7 +238,6 @@ func (tags *TagsInfo) UnmarshalJSON(b []byte) error { case Refund: tags.Refund = append(tags.Refund, parsedPubkey) } - } case NSigs: @@ -280,7 +276,6 @@ func (tags *TagsInfo) UnmarshalJSON(b []byte) error { tags.Locktime = uint(locktime) } - } tags.originalTag = string(b) return nil @@ -319,7 +314,6 @@ func (sc *SpendCondition) VerifyPreimage(witness *Witness) error { } return nil - } type Tags int @@ -405,8 +399,8 @@ type Witness struct { func (wit *Witness) String() (string, error) { var witness = struct { - Preimage string - Signatures []string + Preimage string `json:"preimage,omitempty"` + Signatures []string `json:"signatures,omitempty"` }{ Preimage: "", Signatures: []string{}, @@ -429,8 +423,8 @@ func (wit *Witness) String() (string, error) { func (wit *Witness) UnmarshalJSON(b []byte) error { var sigs = struct { - Preimage string - Signatures []string + Preimage string `json:"preimage,omitempty"` + Signatures []string `json:"signatures,omitempty"` }{ Preimage: "", Signatures: []string{}, @@ -461,13 +455,11 @@ func (wit *Witness) UnmarshalJSON(b []byte) error { } witness.Signatures = append(witness.Signatures, signature) - } *wit = witness return nil - } type SigflagValidation struct { diff --git a/api/cashu/spend_condition_test.go b/api/cashu/spend_condition_test.go index cbc40242..87c747ef 100644 --- a/api/cashu/spend_condition_test.go +++ b/api/cashu/spend_condition_test.go @@ -31,7 +31,6 @@ func init() { } func TestParseProofWithP2PK(t *testing.T) { - var proof Proof err := json.Unmarshal([]byte(singleProofWithP2PK), &proof) @@ -81,9 +80,7 @@ func TestParseProofWithP2PK(t *testing.T) { } if hex.EncodeToString(p2pkWitness.Signatures[0].Serialize()) != "83b585b5d719e95c1cef8514b14b3a027a2053fe174a1b693051c6e2dcbcf6478b4759e5a25a36a0fd67eae392b3a73afa6677b80d1edbbb6b0a9837ef8c413d" { - t.Errorf("Error in p2pkWitness[0] %+v", p2pkWitness.Signatures[0]) - } } @@ -254,7 +251,6 @@ func TestValidSignatureAndInvalidPreimageHTLC(t *testing.T) { // INFO: Testing test vectors for nut11 func TestVectorValidProof(t *testing.T) { - var proof Proof proofString := `{ "amount": 1, @@ -279,11 +275,8 @@ func TestVectorValidProof(t *testing.T) { } if valid != true { - t.Error("proof should be valid") - } - } func TestVectorInvalidProofSignature(t *testing.T) { @@ -342,9 +335,7 @@ func TestVectorValid2Signatures(t *testing.T) { } if valid != true { - t.Error("proof should be valid") - } } @@ -404,9 +395,7 @@ func TestVectorRefundKeySpendable(t *testing.T) { } if valid != true { - t.Error("proof should be valid") - } } diff --git a/api/cashu/swap.go b/api/cashu/swap.go index 391ad453..a3d7625b 100644 --- a/api/cashu/swap.go +++ b/api/cashu/swap.go @@ -6,8 +6,8 @@ import ( ) type PostSwapRequest struct { - Inputs Proofs `json:"inputs"` - Outputs []BlindedMessage `json:"outputs"` + Inputs Proofs `json:"inputs"` + Outputs BlindedMessages `json:"outputs"` } func (p *PostSwapRequest) ValidateSigflag() error { @@ -86,7 +86,6 @@ func (p *PostSwapRequest) verifyConditions() error { if spendCondition.Data.Tags.originalTag != firstSpendCondition.Data.Tags.originalTag { return fmt.Errorf("not same tags %w", ErrInvalidSpendCondition) } - } return nil } diff --git a/api/cashu/swap_test.go b/api/cashu/swap_test.go index 128f7f32..efa4c50c 100644 --- a/api/cashu/swap_test.go +++ b/api/cashu/swap_test.go @@ -45,7 +45,6 @@ func TestSwapRequestMsg(t *testing.T) { if hex.EncodeToString(hashMessage[:]) != "de7f9e3ca0fcc5ed3258fcf83dbf1be7fa78a5ed6da7bf2aa60d61e9dc6eb09a" { t.Errorf("hash message is wrong %v", msg) } - } func TestSwapRequestValidSignature(t *testing.T) { @@ -157,7 +156,6 @@ func TestSwapRequestValidMultiSig(t *testing.T) { err = swapRequest.ValidateSigflag() if err != nil { t.Errorf("there should not have been any error on multisig! %+v ", err) - } } diff --git a/api/cashu/types.go b/api/cashu/types.go index 15bf5a9c..8cd48601 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -53,7 +53,6 @@ var validate *validator.Validate func init() { validate = validator.New() - } const ( @@ -112,7 +111,6 @@ type BlindedMessage struct { } func (b BlindedMessage) GenerateBlindSignature(k *secp256k1.PrivateKey) (BlindSignature, error) { - C_ := crypto.SignBlindedMessage(b.B_.PublicKey, k) blindSig := BlindSignature{ @@ -130,6 +128,16 @@ func (b BlindedMessage) GenerateBlindSignature(k *secp256k1.PrivateKey) (BlindSi return blindSig, nil } +type BlindedMessages []BlindedMessage + +func (p *BlindedMessages) Amount() uint64 { + amount := uint64(0) + for i := 0; i < len(*p); i++ { + amount += (*p)[i].Amount + } + return amount +} + type BlindSignature struct { C_ WrappedPublicKey `json:"C_"` Dleq *BlindSignatureDLEQ `json:"dleq,omitempty"` @@ -312,7 +320,6 @@ func (m *MintRequestDB) PostMintQuoteBolt11Response() PostMintQuoteBolt11Respons if m.Amount != nil { res.Amount = m.Amount - } return res } @@ -320,7 +327,7 @@ func (m *MintRequestDB) PostMintQuoteBolt11Response() PostMintQuoteBolt11Respons type PostMintBolt11Request struct { Signature *schnorr.Signature `json:"signature,omitempty"` Quote string `json:"quote"` - Outputs []BlindedMessage `json:"outputs"` + Outputs BlindedMessages `json:"outputs"` } func (p *PostMintBolt11Request) UnmarshalJSON(data []byte) error { @@ -329,7 +336,8 @@ func (p *PostMintBolt11Request) UnmarshalJSON(data []byte) error { Quote string `json:"quote"` Outputs []BlindedMessage `json:"outputs"` } - if err := json.Unmarshal(data, &aux); err != nil { + err := json.Unmarshal(data, &aux) + if err != nil { return fmt.Errorf("could not marshall into PostMintBolt11Request: %w", err) } @@ -615,7 +623,6 @@ func (b *BlindSignature) VerifyDLEQ( // I negate the hashed_keys_priv because the original key got altered when multiplying for A return hashed_keys_priv.Key.Negate().String() == e.Key.String(), nil - } type MeltChange struct { diff --git a/api/cashu/types_test.go b/api/cashu/types_test.go index 1581687b..2df45919 100644 --- a/api/cashu/types_test.go +++ b/api/cashu/types_test.go @@ -58,7 +58,6 @@ func TestGenerateDLEQ(t *testing.T) { if !verify { t.Errorf("DLEQ is not correct") } - } func TestCashuAmountChangeSatToMsat(t *testing.T) { @@ -305,7 +304,6 @@ func TestNut20SuccessfulSignature(t *testing.T) { if !valid { t.Error("signature should be valid") } - } func TestNut20FailureSignature(t *testing.T) { jsonStr := `{ @@ -375,5 +373,4 @@ func TestNut20FailureSignature(t *testing.T) { if valid { t.Error("signature should be valid") } - } diff --git a/api/cashu/util.go b/api/cashu/util.go index 76515b96..70c4219e 100644 --- a/api/cashu/util.go +++ b/api/cashu/util.go @@ -31,18 +31,15 @@ func OrderKeysetByUnit(keysets []MintKey) KeysResponse { keysetResponse.FinalExpiry = value[0].FinalExpiry for _, keyset := range value { - keysetResponse.Keys[strconv.FormatUint(keyset.Amount, 10)] = hex.EncodeToString(keyset.PrivKey.PubKey().SerializeCompressed()) } res["keysets"] = append(res["keysets"], keysetResponse) } return res - } func GenerateNonceHex() (string, error) { - // generate random Nonce nonce := make([]byte, 32) // create a slice with length 16 for the nonce _, err := rand.Read(nonce) // read random bytes into the nonce slice @@ -63,23 +60,18 @@ func Fees(proofs []Proof, keysets []BasicKeysetResponse) (uint, error) { if keysetToUse.Id != proof.Id { for _, keyset := range keysets { if keyset.Id == proof.Id { - keysetToUse = keyset } } if keysetToUse.Id != proof.Id { return 0, ErrKeysetForProofNotFound - } - } totalFees += keysetToUse.InputFeePpk - } totalFees = (totalFees + 999) / 1000 return totalFees, nil - } diff --git a/api/cashu/util_test.go b/api/cashu/util_test.go index a0de71ba..d499044a 100644 --- a/api/cashu/util_test.go +++ b/api/cashu/util_test.go @@ -47,11 +47,9 @@ func TestOrderKeysetByUnit(t *testing.T) { if firstOrdKey.Keys["1"] != "03a524f43d6166ad3567f18b0a5c769c6ab4dc02149f4d5095ccf4e8ffa293e785" { t.Errorf("keyset is not correct. %v", firstOrdKey.Keys["1"]) } - } func TestAmountOfFeeProofs(t *testing.T) { - var proofs []cashu.Proof var keysets []cashu.BasicKeysetResponse id := "keysetID" @@ -121,5 +119,4 @@ func TestAmountOfFeeProofs(t *testing.T) { if fee != 2 { t.Errorf("fee calculation is incorrect: %v. Should be 2", fee) } - } diff --git a/cmd/nutmix/admin_test.go b/cmd/nutmix/admin_test.go index 104ce74c..19686fca 100644 --- a/cmd/nutmix/admin_test.go +++ b/cmd/nutmix/admin_test.go @@ -143,7 +143,6 @@ func TestSetupMintAdminLoginSuccess(t *testing.T) { if w.Code != 200 { t.Errorf("Expected status code 200, got %d", w.Code) } - } func TestSetupMintAdminLoginFailure(t *testing.T) { diff --git a/cmd/nutmix/htlc_route_test.go b/cmd/nutmix/htlc_route_test.go index 90df54dd..06d75d38 100644 --- a/cmd/nutmix/htlc_route_test.go +++ b/cmd/nutmix/htlc_route_test.go @@ -210,11 +210,9 @@ func TestRoutesHTLCSwapMelt(t *testing.T) { if errorResponse.Code != cashu.PROOF_VERIFICATION_FAILED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Proof could not be verified" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // TRY SWAPING with WRONG Preimage @@ -260,7 +258,6 @@ func TestRoutesHTLCSwapMelt(t *testing.T) { if *errorRes.Detail != `invalid preimage` { t.Fatalf("Expected response Invalid preimage, got %s", w.Body.String()) } - } func CreateHTLCBlindedMessages(amount uint64, keyset signer.GetKeysResponse, preimage string, nSigs uint, pubkeys []*secp256k1.PublicKey, refundPubkey []*secp256k1.PublicKey, locktime uint, sigflag cashu.SigFlag) ([]cashu.BlindedMessage, []string, []*secp256k1.PrivateKey, error) { @@ -310,7 +307,6 @@ func CreateHTLCBlindedMessages(amount uint64, keyset signer.GetKeysResponse, pre } func makeHTLCSpendCondition(preimage string, nSigs uint, pubkeys []*secp256k1.PublicKey, refundPubkey []*secp256k1.PublicKey, locktime uint, sigflag cashu.SigFlag) (cashu.SpendCondition, error) { - bytesPreimage, err := hex.DecodeString(preimage) if err != nil { return cashu.SpendCondition{}, err @@ -340,10 +336,9 @@ func makeHTLCSpendCondition(preimage string, nSigs uint, pubkeys []*secp256k1.Pu func GenerateProofsHTLC(signatures []cashu.BlindSignature, preimage string, keyset signer.GetKeysResponse, secrets []string, secretsKey []*secp256k1.PrivateKey, privkeys []*secp256k1.PrivateKey) ([]cashu.Proof, error) { // try to swap tokens - var proofs []cashu.Proof + var proofs = make([]cashu.Proof, len(signatures)) // unblid the signatures and make proofs for i, output := range signatures { - pubkeyStr := keyset.Keysets[0].Keys[output.Amount] pubkeyBytes, err := hex.DecodeString(pubkeyStr) if err != nil { @@ -374,7 +369,7 @@ func GenerateProofsHTLC(signatures []cashu.BlindSignature, preimage string, keys return nil, fmt.Errorf("Error signing proof: %w", err) } - proofs = append(proofs, proof) + proofs[i] = proof } return proofs, nil @@ -641,11 +636,9 @@ func TestHTLCMultisigSigning(t *testing.T) { if errorResponse.Code != cashu.PROOF_VERIFICATION_FAILED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Proof could not be verified" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // Try swapping with not enough signatures @@ -683,11 +676,9 @@ func TestHTLCMultisigSigning(t *testing.T) { if errorResponse.Code != cashu.PROOF_VERIFICATION_FAILED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Proof could not be verified" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // Try swapping with correct signatures but wrong preimage @@ -762,5 +753,4 @@ func TestHTLCMultisigSigning(t *testing.T) { if w.Code != 200 { t.Fatalf("Expected status code 200, got %d", w.Code) } - } diff --git a/cmd/nutmix/main.go b/cmd/nutmix/main.go index bf575c17..e9eecec6 100644 --- a/cmd/nutmix/main.go +++ b/cmd/nutmix/main.go @@ -40,7 +40,6 @@ var ( ) func main() { - logsdir, err := utils.GetLogsDirectory() if err != nil { log.Panicln("Could not get Logs directory") @@ -59,7 +58,8 @@ func main() { log.Panicf("os.OpenFile(pathToProjectLogFile, os.O_RDWR|os.O_CREATE, 0764) %+v", err) } defer func() { - if err := logFile.Close(); err != nil { + err := logFile.Close() + if err != nil { slog.Warn("failed to close log file", slog.Any("error", err)) } }() @@ -220,5 +220,4 @@ func GetSignerFromValue(signerType string, db database.MintDB) (signer.Signer, e default: return nil, fmt.Errorf("no signer type has been selected") } - } diff --git a/cmd/nutmix/main_test.go b/cmd/nutmix/main_test.go index e7833081..f12219f1 100644 --- a/cmd/nutmix/main_test.go +++ b/cmd/nutmix/main_test.go @@ -51,7 +51,6 @@ var ( ) func TestMintBolt11FakeWallet(t *testing.T) { - const posgrespassword = "password" const postgresuser = "user" ctx := context.Background() @@ -68,7 +67,8 @@ func TestMintBolt11FakeWallet(t *testing.T) { // Better setup: Use t.Cleanup to ensure container is killed even if test panics if postgresContainer != nil { t.Cleanup(func() { - if err := postgresContainer.Terminate(context.Background()); err != nil { + err := postgresContainer.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) @@ -120,7 +120,6 @@ func TestMintBolt11FakeWallet(t *testing.T) { if postMintQuoteResponse.State != cashu.UNPAID { t.Errorf("Expected state to be UNPAID, got %v", postMintQuoteResponse.State) - } if postMintQuoteResponse.Unit != "sat" { @@ -145,7 +144,6 @@ func TestMintBolt11FakeWallet(t *testing.T) { if postMintQuoteResponse.State != cashu.UNPAID { t.Errorf("Expected state to be UNPAID, got %v", postMintQuoteResponse.State) - } if postMintQuoteResponseTwo.Unit != "sat" { @@ -159,19 +157,19 @@ func TestMintBolt11FakeWallet(t *testing.T) { t.Fatalf("mint.Signer.GetKeysByUnit(cashu.Sat): %v", err) } - // ASK FOR MINTING WITH TOO MANY BLINDED MESSAGES + // ASK FOR MINTING WITH OUTPUTS THAT EXCEED THE QUOTE AMOUNT blindedMessages, _, _, err := CreateBlindedMessages(999999, activeKeys) if err != nil { t.Fatalf("could not createBlind message: %v", err) } - mintRequestTooManyBlindMessages := cashu.PostMintBolt11Request{ + amountMismatchMintRequest := cashu.PostMintBolt11Request{ Quote: postMintQuoteResponse.Quote, Outputs: blindedMessages, Signature: nil, } - jsonRequestBody, _ = json.Marshal(mintRequestTooManyBlindMessages) + jsonRequestBody, _ = json.Marshal(amountMismatchMintRequest) req = httptest.NewRequest("POST", "/v1/mint/bolt11", strings.NewReader(string(jsonRequestBody))) @@ -179,12 +177,60 @@ func TestMintBolt11FakeWallet(t *testing.T) { router.ServeHTTP(w, req) - if w.Code != 403 { - t.Fatalf("Expected status code 200, got %d", w.Code) + amountMismatchResponse := cashu.ErrorResponse{} //nolint:exhaustruct + err = json.Unmarshal(w.Body.Bytes(), &amountMismatchResponse) + if err != nil { + t.Fatalf("Could not parse error response %s", w.Body.String()) + } + + if w.Code != 400 { + t.Fatalf("Expected status code 400, got %d", w.Code) + } + + if amountMismatchResponse.Code != cashu.AMOUNT_NOT_EQUAL_TO_INVOICE { + t.Errorf("Incorrect error code, got %v", amountMismatchResponse.Code) + } + + if amountMismatchResponse.Error != cashu.AMOUNT_NOT_EQUAL_TO_INVOICE.String() { + t.Errorf("Incorrect error string, got %s", amountMismatchResponse.Error) + } + + // ASK FOR MINTING WITH OUTPUTS THAT UNDERSHOOT THE QUOTE AMOUNT + blindedMessages, _, _, err = CreateBlindedMessages(9999, activeKeys) + if err != nil { + t.Fatalf("could not createBlind message: %v", err) + } + + amountMismatchMintRequest = cashu.PostMintBolt11Request{ + Quote: postMintQuoteResponse.Quote, + Outputs: blindedMessages, + Signature: nil, + } + + jsonRequestBody, _ = json.Marshal(amountMismatchMintRequest) + + req = httptest.NewRequest("POST", "/v1/mint/bolt11", strings.NewReader(string(jsonRequestBody))) + + w = httptest.NewRecorder() + + router.ServeHTTP(w, req) + + amountMismatchResponse = cashu.ErrorResponse{} //nolint:exhaustruct + err = json.Unmarshal(w.Body.Bytes(), &amountMismatchResponse) + if err != nil { + t.Fatalf("Could not parse error response %s", w.Body.String()) + } + + if w.Code != 400 { + t.Fatalf("Expected status code 400, got %d", w.Code) + } + + if amountMismatchResponse.Code != cashu.AMOUNT_NOT_EQUAL_TO_INVOICE { + t.Errorf("Incorrect error code, got %v", amountMismatchResponse.Code) } - if w.Body.String() != `"Amounts in outputs are not the same"` { - t.Errorf("Expected Amounts in outputs are not the same, got %s", w.Body.String()) + if amountMismatchResponse.Error != cashu.AMOUNT_NOT_EQUAL_TO_INVOICE.String() { + t.Errorf("Incorrect error string, got %s", amountMismatchResponse.Error) } // ASK FOR SUCCESSFUL MINTING @@ -279,11 +325,9 @@ func TestMintBolt11FakeWallet(t *testing.T) { if errorResponse.Code != 20002 { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Quote has already been issued" { t.Fatalf("Incorrect error string, got %s", errorResponse.Error) - } // Minting with invalid signatures @@ -387,11 +431,9 @@ func TestMintBolt11FakeWallet(t *testing.T) { if errorResponse.Code != cashu.TRANSACTION_NOT_BALANCED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Transaction is not balanced (inputs != outputs)" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // TRY TO SWAP SUCCESSFULLY @@ -484,11 +526,9 @@ func TestMintBolt11FakeWallet(t *testing.T) { if errorResponse.Code != cashu.PROOF_VERIFICATION_FAILED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Proof could not be verified" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } w.Flush() @@ -526,11 +566,9 @@ func TestMintBolt11FakeWallet(t *testing.T) { if errorResponse.Code != cashu.TRANSACTION_NOT_BALANCED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Transaction is not balanced (inputs != outputs)" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } w.Flush() @@ -579,7 +617,6 @@ func TestMintBolt11FakeWallet(t *testing.T) { if err != nil { t.Fatalf("Error unmarshalling response: %v", err) - } if postMeltQuoteResponse.State != cashu.UNPAID { @@ -677,17 +714,13 @@ func TestMintBolt11FakeWallet(t *testing.T) { if errorResponse.Code != 20006 { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Invoice already paid" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } - } 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) @@ -821,7 +854,6 @@ func CreateBlindedMessages(amount uint64, keyset signer.GetKeysResponse) ([]cash } func TestMintBolt11LndLigthning(t *testing.T) { - const posgrespassword = "password" const postgresuser = "user" postgresContainer, err := postgres.Run(t.Context(), "postgres:16.2", @@ -836,7 +868,8 @@ func TestMintBolt11LndLigthning(t *testing.T) { // Better setup: Use t.Cleanup to ensure container is killed even if test panics if postgresContainer != nil { t.Cleanup(func() { - if err := postgresContainer.Terminate(context.Background()); err != nil { + err := postgresContainer.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) @@ -865,28 +898,32 @@ func TestMintBolt11LndLigthning(t *testing.T) { // Better setup: Use t.Cleanup to ensure container is killed even if test panics if aliceLnd != nil { t.Cleanup(func() { - if err := aliceLnd.Terminate(context.Background()); err != nil { + err := aliceLnd.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) } if bobLnd != nil { t.Cleanup(func() { - if err := bobLnd.Terminate(context.Background()); err != nil { + err := bobLnd.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) } if btcD != nil { t.Cleanup(func() { - if err := btcD.Terminate(context.Background()); err != nil { + err := btcD.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) } if lnbitsAlice != nil { t.Cleanup(func() { - if err := lnbitsAlice.Terminate(context.Background()); err != nil { + err := lnbitsAlice.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) @@ -901,7 +938,6 @@ func TestMintBolt11LndLigthning(t *testing.T) { } LightningBolt11Test(t, ctx, bobLnd) - } func TestMintBolt11LNBITSLigthning(t *testing.T) { const posgrespassword = "password" @@ -941,28 +977,32 @@ func TestMintBolt11LNBITSLigthning(t *testing.T) { // Better setup: Use t.Cleanup to ensure container is killed even if test panics if aliceLnd != nil { t.Cleanup(func() { - if err := aliceLnd.Terminate(context.Background()); err != nil { + err := aliceLnd.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) } if bobLnd != nil { t.Cleanup(func() { - if err := bobLnd.Terminate(context.Background()); err != nil { + err := bobLnd.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) } if btcD != nil { t.Cleanup(func() { - if err := btcD.Terminate(context.Background()); err != nil { + err := btcD.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) } if lnbitsAlice != nil { t.Cleanup(func() { - if err := lnbitsAlice.Terminate(context.Background()); err != nil { + err := lnbitsAlice.Terminate(context.Background()) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) @@ -976,12 +1016,10 @@ func TestMintBolt11LNBITSLigthning(t *testing.T) { } func GenerateProofs(signatures []cashu.BlindSignature, keyset signer.GetKeysResponse, secrets []string, secretsKey []*secp256k1.PrivateKey) ([]cashu.Proof, error) { - // try to swap tokens - var proofs []cashu.Proof + var proofs = make([]cashu.Proof, len(signatures)) // unblid the signatures and make proofs for i, output := range signatures { - pubkeyStr := keyset.Keysets[0].Keys[output.Amount] pubkeyBytes, err := hex.DecodeString(pubkeyStr) if err != nil { @@ -994,7 +1032,7 @@ func GenerateProofs(signatures []cashu.BlindSignature, keyset signer.GetKeysResp C := crypto.UnblindSignature(output.C_.PublicKey, secretsKey[i], mintPublicKey) - proofs = append(proofs, cashu.Proof{ + proofs[i] = cashu.Proof{ Id: output.Id, Amount: output.Amount, C: cashu.WrappedPublicKey{PublicKey: C}, @@ -1004,7 +1042,7 @@ func GenerateProofs(signatures []cashu.BlindSignature, keyset signer.GetKeysResp Witness: "", State: "", SeenAt: 0, - }) + } } return proofs, nil @@ -1110,11 +1148,9 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer if errorResponse.Code != 20001 { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Quote request is not paid" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // needs to wait a second for the containers to catch up @@ -1166,19 +1202,19 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Code) } - // ASK FOR MINTING WITH TOO MANY BLINDED MESSAGES + // ASK FOR MINTING WITH OUTPUTS THAT EXCEED THE QUOTE AMOUNT blindedMessages, _, _, err := CreateBlindedMessages(999999, activeKeys) if err != nil { t.Fatalf("could not createBlind message: %v", err) } - mintRequestTooManyBlindMessages := cashu.PostMintBolt11Request{ + amountMismatchMintRequest := cashu.PostMintBolt11Request{ Quote: postMintQuoteResponse.Quote, Outputs: blindedMessages, Signature: nil, } - jsonRequestBody, _ = json.Marshal(mintRequestTooManyBlindMessages) + jsonRequestBody, _ = json.Marshal(amountMismatchMintRequest) req = httptest.NewRequest("POST", "/v1/mint/bolt11", strings.NewReader(string(jsonRequestBody))) @@ -1186,12 +1222,22 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer router.ServeHTTP(w, req) - if w.Code != 403 { - t.Fatalf("Expected status code 403, got %d", w.Code) + errorResponse = cashu.ErrorResponse{} //nolint:exhaustruct + err = json.Unmarshal(w.Body.Bytes(), &errorResponse) + if err != nil { + t.Fatalf("Could not parse error response %s", w.Body.String()) + } + + if w.Code != 400 { + t.Fatalf("Expected status code 400, got %d", w.Code) + } + + if errorResponse.Code != cashu.AMOUNT_NOT_EQUAL_TO_INVOICE { + t.Errorf("Incorrect error code, got %v", errorResponse.Code) } - if w.Body.String() != `"Amounts in outputs are not the same"` { - t.Errorf("Expected Amounts in outputs are not the same, got %s", w.Body.String()) + if errorResponse.Error != cashu.AMOUNT_NOT_EQUAL_TO_INVOICE.String() { + t.Errorf("Incorrect error string, got %s", errorResponse.Error) } // MINT SUCCESSFULLY @@ -1286,11 +1332,9 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer if errorResponse.Code != 20002 { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Quote has already been issued" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // MINTING TESTING ENDS @@ -1331,11 +1375,9 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer if errorResponse.Code != cashu.TRANSACTION_NOT_BALANCED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Transaction is not balanced (inputs != outputs)" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // try to swap tokens @@ -1428,11 +1470,9 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer if errorResponse.Code != cashu.PROOF_VERIFICATION_FAILED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Proof could not be verified" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } w.Flush() @@ -1470,11 +1510,9 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer if errorResponse.Code != cashu.TRANSACTION_NOT_BALANCED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Transaction is not balanced (inputs != outputs)" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } w.Flush() @@ -1552,7 +1590,6 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer } if postMeltQuoteResponse.State != cashu.UNPAID { - t.Errorf("Expected to not be paid have: %s ", postMintQuoteResponseTwo.State) } @@ -1644,11 +1681,9 @@ func LightningBolt11Test(t *testing.T, ctx context.Context, bobLnd testcontainer if errorResponse.Code != 20006 { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Invoice already paid" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // MELTING TESTING ENDS @@ -1715,11 +1750,9 @@ func TestWrongUnitOnMeltAndMint(t *testing.T) { if errorResponse.Code != cashu.UNIT_NOT_SUPPORTED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Unit in request is not supported" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } // melt quote with incorrect unit @@ -1744,16 +1777,13 @@ func TestWrongUnitOnMeltAndMint(t *testing.T) { if errorResponse.Code != cashu.UNIT_NOT_SUPPORTED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Unit in request is not supported" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } } func TestConfigMeltMintLimit(t *testing.T) { - const posgrespassword = "password" const postgresuser = "user" ctx := context.Background() @@ -1810,10 +1840,21 @@ func TestConfigMeltMintLimit(t *testing.T) { router.ServeHTTP(w, req) if w.Code != 400 { - t.Errorf("Expected status code 200, got %d", w.Code) + t.Errorf("Expected status code 400, got %d", w.Code) } - if w.Body.String() != `"Mint amount over the limit"` { - t.Errorf(`Expected body message to be: "Mint amount over the limit". Got: %s`, w.Body.String()) + errorResponse := cashu.ErrorResponse{} //nolint:exhaustruct + 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.INSUFICIENT_OUTSIDE_LIMIT { + t.Errorf("Expected code to be Amount outside limit. Got: %v", errorResponse.Code) + } + if errorResponse.Error != "Amount outside limit" { + t.Errorf("Expected error to be Amount outside limit. Got: %s", errorResponse.Error) + } + if errorResponse.Detail == nil || *errorResponse.Detail != "amount is outside the limit" { + t.Errorf("Expected detail to be amount is outside the limit. Got: %v", errorResponse.Detail) } w = httptest.NewRecorder() @@ -1825,9 +1866,9 @@ func TestConfigMeltMintLimit(t *testing.T) { router.ServeHTTP(w, req) if w.Code != 400 { - t.Errorf("Expected status code 200, got %d", w.Code) + t.Errorf("Expected status code 400, got %d", w.Code) } - errorResponse := cashu.ErrorResponse{} //nolint:exhaustruct + errorResponse = cashu.ErrorResponse{} //nolint:exhaustruct err = json.Unmarshal(w.Body.Bytes(), &errorResponse) if err != nil { @@ -1840,7 +1881,6 @@ func TestConfigMeltMintLimit(t *testing.T) { if errorResponse.Error != "Minting is disabled" { t.Errorf(`Expected code be Minting disables. Got: %s`, errorResponse.Error) } - } func TestFeeReturnAmount(t *testing.T) { const posgrespassword = "password" @@ -2003,6 +2043,5 @@ func TestFeeReturnAmount(t *testing.T) { if changeAmount != 9000 { t.Errorf("Change amount is incorrect %v", changeAmount) - } } diff --git a/cmd/nutmix/p2pk_route_test.go b/cmd/nutmix/p2pk_route_test.go index 444ff395..93771296 100644 --- a/cmd/nutmix/p2pk_route_test.go +++ b/cmd/nutmix/p2pk_route_test.go @@ -197,13 +197,10 @@ func TestRoutesP2PKSwapMelt(t *testing.T) { if errorResponse.Code != cashu.PROOF_VERIFICATION_FAILED { t.Errorf("Incorrect error code, got %v", errorResponse.Code) - } if errorResponse.Error != "Proof could not be verified" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } - } func CreateP2PKBlindedMessages(amount uint64, keyset signer.GetKeysResponse, pubkey *secp256k1.PublicKey, nSigs uint, pubkeys []*secp256k1.PublicKey, refundPubkey []*secp256k1.PublicKey, locktime uint, sigflag cashu.SigFlag) ([]cashu.BlindedMessage, []string, []*secp256k1.PrivateKey, error) { @@ -274,10 +271,9 @@ func makeP2PKSpendCondition(pubkey *secp256k1.PublicKey, nSigs uint, pubkeys []* func GenerateProofsP2PK(signatures []cashu.BlindSignature, keyset signer.GetKeysResponse, secrets []string, secretsKey []*secp256k1.PrivateKey, privkeys []*secp256k1.PrivateKey) ([]cashu.Proof, error) { // try to swap tokens - var proofs []cashu.Proof + var proofs = make([]cashu.Proof, len(signatures)) // unblid the signatures and make proofs for i, output := range signatures { - pubkeyStr := keyset.Keysets[0].Keys[output.Amount] pubkeyBytes, err := hex.DecodeString(pubkeyStr) if err != nil { @@ -303,7 +299,7 @@ func GenerateProofsP2PK(signatures []cashu.BlindSignature, keyset signer.GetKeys return nil, fmt.Errorf("Error signing proof: %w", err) } - proofs = append(proofs, proof) + proofs[i] = proof } return proofs, nil @@ -572,7 +568,5 @@ func TestP2PKMultisigSigning(t *testing.T) { } if errorResponse.Error != "Proof could not be verified" { t.Errorf("Incorrect error string, got %s", errorResponse.Error) - } - } diff --git a/cmd/nutmix/payment_error_handling_test.go b/cmd/nutmix/payment_error_handling_test.go index b31c7422..87cab8eb 100644 --- a/cmd/nutmix/payment_error_handling_test.go +++ b/cmd/nutmix/payment_error_handling_test.go @@ -3,6 +3,7 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "net/http/httptest" "strings" @@ -179,7 +180,7 @@ func TestPaymentFailureButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { t.Fatalf("Could not parse error response %s", w.Body.String()) } - if errorResponse.Code != cashu.INVOICE_ALREADY_PAID { + if errorResponse.Code != cashu.QUOTE_PENDING { t.Errorf("Incorrect error code, got %v", errorResponse.Code) } @@ -202,7 +203,6 @@ func TestPaymentFailureButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { t.Fatalf("mint.MintDB.Commit(ctx, tx) %s", err) return } - } func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { @@ -233,9 +233,7 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { t.Setenv(mint.NETWORK_ENV, "regtest") t.Setenv("DATABASE_URL", connUri) - router, mint := SetupRoutingForTesting(ctx, false) - - w := httptest.NewRecorder() + _, mintInstance := SetupRoutingForTesting(ctx, false) mintQuoteRequest := cashu.PostMintQuoteBolt11Request{ Description: nil, @@ -243,26 +241,14 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { 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) - + postMintQuoteResponse, err := mintInstance.CreateMintQuote(ctx, mintQuoteRequest, mint.Bolt11) if err != nil { - t.Errorf("Error unmarshalling response: %v", err) + t.Fatalf("mintInstance.CreateMintQuote(ctx, mintQuoteRequest, mint.Bolt11): %v", err) } - activeKeys, err := mint.Signer.GetActiveKeys() + activeKeys, err := mintInstance.Signer.GetActiveKeys() if err != nil { - t.Fatalf("mint.Signer.GetKeysByUnit(cashu.Sat): %v", err) + t.Fatalf("mintInstance.Signer.GetKeysByUnit(cashu.Sat): %v", err) } // ASK FOR SUCCESSFUL MINTING @@ -277,24 +263,9 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { Signature: nil, } - 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) - + postMintResponse, err := mintInstance.Mint(ctx, mintRequest, mint.Bolt11) if err != nil { - t.Fatalf("Error unmarshalling response: %v", err) + t.Fatalf("mintInstance.Mint(ctx, mintRequest, mint.Bolt11): %v", err) } /// start doing melt quote @@ -303,34 +274,22 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { Request: RegtestRequest, Options: cashu.PostMeltQuoteBolt11Options{Mpp: nil}, } - - 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) - + postMeltQuoteResponse, err := mintInstance.MeltQuote(ctx, meltQuoteRequest, mint.Bolt11) if err != nil { - t.Fatalf("Error unmarshalling response: %v", err) + t.Fatalf("mintInstance.MeltQuote(ctx, meltQuoteRequest, mint.Bolt11): %v", err) } // try melting - - w.Flush() // errors to lightning to force payment checking fakeWallet := lightning.FakeWallet{ - Network: *mint.LightningBackend.GetNetwork(), + Network: *mintInstance.LightningBackend.GetNetwork(), UnpurposeErrors: []lightning.FakeWalletError{ lightning.FailPaymentFailed, lightning.FailQueryPending, }, InvoiceFee: 0, } - mint.LightningBackend = &fakeWallet + mintInstance.LightningBackend = &fakeWallet meltProofs, err := GenerateProofs(postMintResponse.Signatures, activeKeys, mintingSecrets, mintingSecretKeys) if err != nil { @@ -344,84 +303,67 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { Outputs: nil, } - 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 - - err = json.Unmarshal(w.Body.Bytes(), &postMeltResponse) - + postMeltResponse, err := mintInstance.Melt(ctx, meltRequest, mint.Bolt11) if err != nil { - t.Fatalf("Error unmarshalling response: %v", err) + t.Fatalf("mintInstance.Melt(ctx, meltRequest, mint.Bolt11): %v", err) } if postMeltResponse.State == cashu.PAID { t.Errorf("Expected state to not be PAID because it's a fake wallet, got %v", postMeltResponse.State) } - tx, err := mint.MintDB.GetTx(ctx) + tx, err := mintInstance.MintDB.GetTx(ctx) if err != nil { - t.Fatalf("mint.MintDB.GetTx(): %+v", err) + t.Fatalf("mintInstance.MintDB.GetTx(): %+v", err) } defer func() { - _ = mint.MintDB.Rollback(ctx, tx) + _ = mintInstance.MintDB.Rollback(ctx, tx) }() - proofs, _ := mint.MintDB.GetProofsFromSecret(tx, []string{meltProofs[0].Secret}) + proofs, err := mintInstance.MintDB.GetProofsFromSecret(tx, []string{meltProofs[0].Secret}) + if err != nil { + t.Fatalf("mintInstance.MintDB.GetProofsFromSecret(tx, []string{meltProofs[0].Secret}): %+v", err) + } if proofs[0].State != cashu.PROOF_PENDING { t.Errorf("Proof should be pending. it is now: %v", proofs[0].State) } - err = mint.MintDB.Commit(ctx, tx) + err = mintInstance.MintDB.Commit(ctx, tx) if err != nil { - t.Fatalf("mint.MintDB.Commit(ctx, tx) %s", err) + t.Fatalf("mintInstance.MintDB.Commit(ctx, tx) %s", err) return } - req = httptest.NewRequest("POST", "/v1/melt/bolt11", strings.NewReader(string(jsonRequestBody))) - w = httptest.NewRecorder() - router.ServeHTTP(w, req) - var errorResponse cashu.ErrorResponse //nolint:exhaustruct - - 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.INVOICE_ALREADY_PAID { - t.Errorf("Incorrect error code, got %v", errorResponse.Code) + _, err = mintInstance.Melt(ctx, meltRequest, mint.Bolt11) + if !errors.Is(err, cashu.ErrQuoteIsPending) { + t.Fatalf("Expected ErrQuoteIsPending, got %v", err) } secreList := []string{} for _, p := range meltProofs { secreList = append(secreList, p.Secret) } - tx, err = mint.MintDB.GetTx(ctx) + tx, err = mintInstance.MintDB.GetTx(ctx) if err != nil { - t.Fatalf("mint.MintDB.GetTx(): %+v", err) + t.Fatalf("mintInstance.MintDB.GetTx(): %+v", err) } defer func() { - _ = mint.MintDB.Rollback(ctx, tx) + _ = mintInstance.MintDB.Rollback(ctx, tx) }() - proofsDB, err := mint.MintDB.GetProofsFromSecret(tx, secreList) + proofsDB, err := mintInstance.MintDB.GetProofsFromSecret(tx, secreList) if err != nil { - t.Fatalf("mint.MintDB.GetProofsFromSecret() %s", w.Body.String()) + t.Fatalf("mintInstance.MintDB.GetProofsFromSecret(): %+v", err) } for _, p := range proofsDB { if p.State != cashu.PROOF_PENDING { t.Errorf("Proof is not pending %+v", p) } } - err = mint.MintDB.Commit(ctx, tx) + err = mintInstance.MintDB.Commit(ctx, tx) if err != nil { - t.Fatalf("mint.MintDB.Commit(ctx, tx) %s", err) + t.Fatalf("mintInstance.MintDB.Commit(ctx, tx) %s", err) return } - } func TestPaymentPendingButPendingCheckPaymentMockDbFakeWallet(t *testing.T) { diff --git a/internal/database/goose/goose.go b/internal/database/goose/goose.go index 332ad36d..9c96cc6c 100644 --- a/internal/database/goose/goose.go +++ b/internal/database/goose/goose.go @@ -16,13 +16,14 @@ const POSTGRES DatabaseType = "postgres" var embedMigrations embed.FS // func RunMigration(db *sql.DB, databaseType DatabaseType) error { - goose.SetBaseFS(embedMigrations) - if err := goose.SetDialect(string(databaseType)); err != nil { + err := goose.SetDialect(string(databaseType)) + if err != nil { return fmt.Errorf(`goose.SetDialect(string(databaseType)). %w`, err) } - if err := goose.Up(db, "migrations"); err != nil { + gooseErr := goose.Up(db, "migrations") + if gooseErr != nil { return fmt.Errorf(`goose.Up(db, "migrations"). %w`, err) } diff --git a/internal/database/mock_db/auth.go b/internal/database/mock_db/auth.go index bd95dd8b..ed00f229 100644 --- a/internal/database/mock_db/auth.go +++ b/internal/database/mock_db/auth.go @@ -9,15 +9,12 @@ import ( ) func (m *MockDB) MakeAuthUser(tx pgx.Tx, auth database.AuthUser) error { - _, err := tx.Exec(context.Background(), "INSERT INTO user_auth (sub, aud , last_logged_in) VALUES ($1, $2, $3)", auth.Sub, auth.Aud, auth.LastLoggedIn) if err != nil { return databaseError(fmt.Errorf("inserting to auth user login: %w", err)) - } return nil - } func (m *MockDB) GetAuthUser(tx pgx.Tx, sub string) (database.AuthUser, error) { @@ -34,7 +31,6 @@ func (m *MockDB) GetAuthUser(tx pgx.Tx, sub string) (database.AuthUser, error) { } return nostrLogin, nil - } func (m *MockDB) UpdateLastLoggedIn(tx pgx.Tx, sub string, lastLoggedIn uint64) error { @@ -42,7 +38,6 @@ func (m *MockDB) UpdateLastLoggedIn(tx pgx.Tx, sub string, lastLoggedIn uint64) _, err := tx.Exec(context.Background(), "UPDATE user_auth SET last_logged_in = $1 WHERE sub = $2", lastLoggedIn, sub) if err != nil { return databaseError(fmt.Errorf("update to seeds: %w", err)) - } return nil } diff --git a/internal/database/mock_db/change.go b/internal/database/mock_db/change.go index c0c2f6d6..aaf00fc6 100644 --- a/internal/database/mock_db/change.go +++ b/internal/database/mock_db/change.go @@ -6,7 +6,6 @@ import ( ) func (m *MockDB) SaveMeltChange(tx pgx.Tx, change []cashu.BlindedMessage, quote string) error { - for _, v := range change { m.MeltChange = append(m.MeltChange, cashu.MeltChange{ B_: v.B_, @@ -14,20 +13,15 @@ func (m *MockDB) SaveMeltChange(tx pgx.Tx, change []cashu.BlindedMessage, quote Quote: quote, CreatedAt: 0, }) - } return nil } func (m *MockDB) GetMeltChangeByQuote(tx pgx.Tx, quote string) ([]cashu.MeltChange, error) { - var change []cashu.MeltChange for i := 0; i < len(m.MeltChange); i++ { - if m.MeltChange[i].Quote == quote { change = append(change, m.MeltChange[i]) - } - } return change, nil @@ -35,11 +29,9 @@ func (m *MockDB) GetMeltChangeByQuote(tx pgx.Tx, quote string) ([]cashu.MeltChan func (m *MockDB) DeleteChangeByQuote(tx pgx.Tx, quote string) error { for i := 0; i < len(m.MeltChange); i++ { - if m.MeltChange[i].Quote == quote { m.MeltChange = append(m.MeltChange[:i], m.MeltChange[i+1:]...) } - } return nil diff --git a/internal/database/postgresql/auth.go b/internal/database/postgresql/auth.go index 6f51d438..878bcc81 100644 --- a/internal/database/postgresql/auth.go +++ b/internal/database/postgresql/auth.go @@ -40,15 +40,12 @@ func (pql Postgresql) GetNostrAuth(tx pgx.Tx, nonce string) (database.NostrLogin } func (pql Postgresql) MakeAuthUser(tx pgx.Tx, auth database.AuthUser) error { - _, err := tx.Exec(context.Background(), "INSERT INTO user_auth (sub, aud , last_logged_in) VALUES ($1, $2, $3)", auth.Sub, auth.Aud, auth.LastLoggedIn) if err != nil { return databaseError(fmt.Errorf("inserting to auth user login: %w", err)) - } return nil - } func (pql Postgresql) GetAuthUser(tx pgx.Tx, sub string) (database.AuthUser, error) { @@ -72,7 +69,6 @@ func (pql Postgresql) UpdateLastLoggedIn(tx pgx.Tx, sub string, lastLoggedIn uin _, err := tx.Exec(context.Background(), "UPDATE user_auth SET last_logged_in = $1 WHERE sub = $2", lastLoggedIn, sub) if err != nil { return databaseError(fmt.Errorf("update to seeds: %w", err)) - } return nil } diff --git a/internal/database/postgresql/change.go b/internal/database/postgresql/change.go index e40b6151..11c25d3d 100644 --- a/internal/database/postgresql/change.go +++ b/internal/database/postgresql/change.go @@ -34,12 +34,10 @@ func (pql Postgresql) SaveMeltChange(tx pgx.Tx, change []cashu.BlindedMessage, q case err == nil: return nil } - } } func (pql Postgresql) GetMeltChangeByQuote(tx pgx.Tx, quote string) ([]cashu.MeltChange, error) { - meltChangeList := make([]cashu.MeltChange, 0) rows, err := tx.Query(context.Background(), `SELECT "B_", id, quote, created_at FROM melt_change_message WHERE quote = $1 FOR UPDATE NOWAIT`, quote) @@ -65,7 +63,6 @@ func (pql Postgresql) GetMeltChangeByQuote(tx pgx.Tx, quote string) ([]cashu.Mel return meltChangeList, nil } func (pql Postgresql) DeleteChangeByQuote(tx pgx.Tx, quote string) error { - _, err := tx.Exec(context.Background(), `DELETE FROM melt_change_message WHERE quote = $1`, quote) if err != nil { diff --git a/internal/database/postgresql/main.go b/internal/database/postgresql/main.go index 6d34fa27..d894d4f8 100644 --- a/internal/database/postgresql/main.go +++ b/internal/database/postgresql/main.go @@ -28,13 +28,11 @@ func databaseError(err error) error { } func DatabaseSetup(ctx context.Context, migrationDir string) (Postgresql, error) { - var postgresql Postgresql dbUrl := os.Getenv(DATABASE_URL_ENV) if dbUrl == "" { return postgresql, fmt.Errorf("%v environment variable empty", DATABASE_URL_ENV) - } pool, err := pgxpool.New(ctx, dbUrl) @@ -110,7 +108,6 @@ func (pql Postgresql) GetSeedsByUnit(tx pgx.Tx, unit cashu.Unit) ([]cashu.Seed, } func (pql Postgresql) SaveNewSeed(tx pgx.Tx, seed cashu.Seed) error { - tries := 0 for { @@ -125,7 +122,6 @@ func (pql Postgresql) SaveNewSeed(tx pgx.Tx, seed cashu.Seed) error { case err == nil: return nil } - } } @@ -152,22 +148,19 @@ func (pql Postgresql) SaveNewSeeds(seeds []cashu.Seed) error { case err == nil: return nil } - } - } func (pql Postgresql) UpdateSeedsActiveStatus(tx pgx.Tx, seeds []cashu.Seed) error { // change the paid status of the quote batch := pgx.Batch{} //nolint:exhaustruct for _, seed := range seeds { - batch.Queue("UPDATE seeds SET active = $1 WHERE id = $2", seed.Active, seed.Id) - } results := tx.SendBatch(context.Background(), &batch) defer func() { - if err := results.Close(); err != nil { + err := results.Close() + if err != nil { slog.Error("failed to close results", slog.Any("error", err)) } }() @@ -179,7 +172,6 @@ func (pql Postgresql) UpdateSeedsActiveStatus(tx pgx.Tx, seeds []cashu.Seed) err defer rows.Close() return nil - } func (pql Postgresql) SaveMintRequest(tx pgx.Tx, request cashu.MintRequestDB) error { @@ -188,7 +180,6 @@ func (pql Postgresql) SaveMintRequest(tx pgx.Tx, request cashu.MintRequestDB) er _, err := tx.Exec(ctx, "INSERT INTO mint_request (quote, request, expiry, unit, minted, state, seen_at, amount, checking_id, pubkey, description) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)", request.Quote, request.Request, request.Expiry, request.Unit, request.Minted, request.State, request.SeenAt, request.Amount, request.CheckingId, request.Pubkey, request.Description) if err != nil { return databaseError(fmt.Errorf("inserting to mint_request: %w", err)) - } return nil } @@ -198,13 +189,11 @@ func (pql Postgresql) ChangeMintRequestState(tx pgx.Tx, quote string, state cash _, err := tx.Exec(context.Background(), "UPDATE mint_request SET state = $2, minted = $3 WHERE quote = $1", quote, state, minted) if err != nil { return databaseError(fmt.Errorf("inserting to mint_request: %w", err)) - } return nil } func (pql Postgresql) GetMintRequestById(tx pgx.Tx, id string) (cashu.MintRequestDB, error) { - var amount *uint64 var mintRequest cashu.MintRequestDB @@ -253,7 +242,6 @@ func (pql Postgresql) GetMeltRequestById(tx pgx.Tx, id string) (cashu.MeltReques } func (pql Postgresql) GetMeltQuotesByState(state cashu.ACTION_STATE) ([]cashu.MeltRequestDB, error) { - rows, err := pql.pool.Query(context.Background(), "SELECT quote, request, amount, expiry, unit, melted, fee_reserve, state, payment_preimage, seen_at, mpp, fee_paid, checking_id FROM melt_request WHERE state = $1", state) if err != nil { return nil, fmt.Errorf("could not find melt requests from state %w", err) @@ -270,7 +258,6 @@ func (pql Postgresql) GetMeltQuotesByState(state cashu.ACTION_STATE) ([]cashu.Me } func (pql Postgresql) SaveMeltRequest(tx pgx.Tx, request cashu.MeltRequestDB) error { - _, err := tx.Exec(context.Background(), "INSERT INTO melt_request (quote, request, fee_reserve, expiry, unit, amount, melted, state, payment_preimage, seen_at, mpp, fee_paid, checking_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)", request.Quote, request.Request, request.FeeReserve, request.Expiry, request.Unit, request.Amount, request.Melted, request.State, request.PaymentPreimage, request.SeenAt, request.Mpp, request.FeePaid, request.CheckingId) @@ -285,7 +272,6 @@ func (pql Postgresql) AddPreimageMeltRequest(tx pgx.Tx, quote string, preimage s _, err := tx.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 } @@ -294,7 +280,6 @@ func (pql Postgresql) ChangeMeltRequestState(tx pgx.Tx, quote string, state cash _, err := tx.Exec(context.Background(), "UPDATE melt_request SET state = $2, melted = $3, fee_paid = $4 WHERE quote = $1", quote, state, melted, fee_paid) if err != nil { return databaseError(fmt.Errorf("updating mint_request: %w", err)) - } return nil } @@ -303,13 +288,11 @@ func (pql Postgresql) ChangeCheckingId(tx pgx.Tx, quote string, checking_id stri _, err := tx.Exec(context.Background(), "UPDATE melt_request SET checking_id = $1 WHERE quote = $2", checking_id, quote) if err != nil { return databaseError(fmt.Errorf("updating mint_request: %w", err)) - } return nil } func (pql Postgresql) GetProofsFromSecret(tx pgx.Tx, SecretList []string) (cashu.Proofs, error) { - var proofList cashu.Proofs ctx := context.Background() @@ -349,14 +332,12 @@ func (pql Postgresql) SaveProof(tx pgx.Tx, proofs []cashu.Proof) error { _, err := tx.CopyFrom(context.Background(), pgx.Identifier{tableName}, columns, pgx.CopyFromRows(entries)) if err != nil { - return databaseError(fmt.Errorf("inserting to DB: %w", err)) } return nil } func (pql Postgresql) GetProofsFromSecretCurve(tx pgx.Tx, Ys []cashu.WrappedPublicKey) (cashu.Proofs, error) { - proofList := make(cashu.Proofs, 0) rows, err := tx.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at, state, quote FROM proofs WHERE y = ANY($1) FOR UPDATE NOWAIT`, Ys) @@ -384,7 +365,6 @@ func (pql Postgresql) GetProofsFromSecretCurve(tx pgx.Tx, Ys []cashu.WrappedPubl } func (pql Postgresql) GetProofsFromQuote(tx pgx.Tx, quote string) (cashu.Proofs, error) { - var proofList cashu.Proofs rows, err := tx.Query(context.Background(), `SELECT amount, id, secret, c, y, witness, seen_at, state, quote FROM proofs WHERE quote = $1 FOR UPDATE NOWAIT`, quote) @@ -417,7 +397,8 @@ func (pql Postgresql) SetProofsState(tx pgx.Tx, proofs cashu.Proofs, state cashu results := tx.SendBatch(context.Background(), &batch) defer func() { - if err := results.Close(); err != nil { + err := results.Close() + if err != nil { slog.Error("failed to close results", slog.Any("error", err)) } }() @@ -440,7 +421,8 @@ func (pql Postgresql) DeleteProofs(tx pgx.Tx, proofs cashu.Proofs) error { results := tx.SendBatch(context.Background(), &batch) defer func() { - if err := results.Close(); err != nil { + err := results.Close() + if err != nil { slog.Error("failed to close results", slog.Any("error", err)) } }() @@ -452,7 +434,6 @@ func (pql Postgresql) DeleteProofs(tx pgx.Tx, proofs cashu.Proofs) error { defer rows.Close() return nil - } func privateKeysToDleq(s_key []byte, e_key []byte, sig *cashu.RecoverSigDB) { @@ -472,7 +453,6 @@ func privateKeysToDleq(s_key []byte, e_key []byte, sig *cashu.RecoverSigDB) { } func (pql Postgresql) GetRestoreSigsFromBlindedMessages(tx pgx.Tx, B_ []cashu.WrappedPublicKey) ([]cashu.RecoverSigDB, error) { - signaturesList := make([]cashu.RecoverSigDB, 0) rows, err := tx.Query(context.Background(), `SELECT id, amount, "C_", "B_", created_at, dleq_e, dleq_s FROM recovery_signature WHERE "B_" = ANY($1)`, B_) @@ -530,7 +510,6 @@ func (pql Postgresql) SaveRestoreSigs(tx pgx.Tx, recover_sigs []cashu.RecoverSig case err == nil: return nil } - } } diff --git a/internal/database/postgresql/operations_test.go b/internal/database/postgresql/operations_test.go index 75b8cd6c..ae02c330 100644 --- a/internal/database/postgresql/operations_test.go +++ b/internal/database/postgresql/operations_test.go @@ -34,7 +34,8 @@ func TestAddAndRequestMintRequestValidPubkey(t *testing.T) { t.Fatal(err) } defer func() { - if err := postgresContainer.Terminate(ctx); err != nil { + err := postgresContainer.Terminate(ctx) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }() @@ -49,7 +50,6 @@ func TestAddAndRequestMintRequestValidPubkey(t *testing.T) { db, err := DatabaseSetup(ctx, "migrations") if err != nil { t.Fatalf("could not setup migration. %v", err) - } pubkeyStr := "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac" @@ -141,7 +141,8 @@ func TestAddAndRequestMintRequestNilPubkey(t *testing.T) { t.Fatal(err) } defer func() { - if err := postgresContainer.Terminate(ctx); err != nil { + err := postgresContainer.Terminate(ctx) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }() @@ -156,7 +157,6 @@ func TestAddAndRequestMintRequestNilPubkey(t *testing.T) { db, err := DatabaseSetup(ctx, "migrations") if err != nil { t.Fatalf("could not setup migration. %v", err) - } quoteId, err := utils.RandomHash() @@ -670,7 +670,8 @@ func setupTestDB(t *testing.T) (Postgresql, context.Context) { t.Fatal(err) } t.Cleanup(func() { - if err := postgresContainer.Terminate(ctx); err != nil { + err := postgresContainer.Terminate(ctx) + if err != nil { t.Fatalf("failed to terminate container: %s", err) } }) diff --git a/internal/lightning/backend.go b/internal/lightning/backend.go index abce4ad7..a1f18555 100644 --- a/internal/lightning/backend.go +++ b/internal/lightning/backend.go @@ -28,7 +28,7 @@ type LightningBackend interface { PayInvoice(melt_quote cashu.MeltRequestDB, zpayInvoice *zpay32.Invoice, feeReserve cashu.Amount, mpp bool, amount cashu.Amount) (PaymentResponse, error) CheckPayed(quote string, invoice *zpay32.Invoice, checkingId string) (PaymentStatus, string, cashu.Amount, error) CheckReceived(quote cashu.MintRequestDB, invoice *zpay32.Invoice) (PaymentStatus, string, error) - RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amount) (InvoiceResponse, error) + RequestInvoice(amount cashu.Amount, description *string) (InvoiceResponse, error) // returns the amount in sats and the checking_id QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mpp bool, amount cashu.Amount) (FeesResponse, error) // returns milisats balance diff --git a/internal/lightning/cln.go b/internal/lightning/cln.go index 848c49af..cc8be318 100644 --- a/internal/lightning/cln.go +++ b/internal/lightning/cln.go @@ -134,7 +134,6 @@ func (l *CLNGRPCWallet) clnGrpcPayInvoice(invoice string, feeReserve cashu.Amoun lightningResponse.PaidFee = feePaidMsat return nil - } func (l *CLNGRPCWallet) clnGrpcPayPartialInvoice(invoice string, _ *zpay32.Invoice, @@ -192,7 +191,6 @@ func (l *CLNGRPCWallet) clnGrpcPayPartialInvoice(invoice string, lightningResponse.PaidFee = feePaidMsat return nil - } func (l CLNGRPCWallet) PayInvoice(melt_quote cashu.MeltRequestDB, zpayInvoice *zpay32.Invoice, feeReserve cashu.Amount, mpp bool, amount cashu.Amount) (PaymentResponse, error) { @@ -250,9 +248,7 @@ func (l CLNGRPCWallet) CheckPayed(quote string, invoice *zpay32.Invoice, checkin return PENDING, hex.EncodeToString(pay.PaymentHash), fee, nil case cln_grpc.ListpaysPays_FAILED: return FAILED, hex.EncodeToString(pay.PaymentHash), fee, nil - } - } return PENDING, "", fee, nil } @@ -279,9 +275,7 @@ func (l CLNGRPCWallet) CheckReceived(quote cashu.MintRequestDB, invoice *zpay32. case cln_grpc.ListinvoicesInvoices_UNPAID: return PENDING, hex.EncodeToString(invoice.PaymentHash), nil - } - } return PENDING, "", nil } @@ -351,7 +345,7 @@ func (l CLNGRPCWallet) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mp return feesResponse, nil } -func (l CLNGRPCWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amount) (InvoiceResponse, error) { +func (l CLNGRPCWallet) RequestInvoice(amount cashu.Amount, description *string) (InvoiceResponse, error) { var response InvoiceResponse supported := l.VerifyUnitSupport(amount.Unit) if !supported { @@ -379,16 +373,14 @@ func (l CLNGRPCWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Am return response, fmt.Errorf(`uuid.NewRandom() %w`, err) } - //nolint:exhaustruct - req := cln_grpc.InvoiceRequest{ - AmountMsat: &cln_grpc.AmountOrAny{ - Value: &amountOrAllCln, - }, - Label: randUuid.String(), - Description: "", + amountOrAny := cln_grpc.AmountOrAny{ + Value: &amountOrAllCln, } - if quote.Description != nil { - req.Description = *quote.Description + var req cln_grpc.InvoiceRequest + req.AmountMsat = &amountOrAny + req.Label = randUuid.String() + if description != nil { + req.Description = *description } // Expiry time is 15 minutes @@ -423,9 +415,7 @@ func (l CLNGRPCWallet) WalletBalance() (cashu.Amount, error) { fundsMSat := uint64(0) for _, channel := range balance.Channels { - fundsMSat += channel.OurAmountMsat.Msat - } return cashu.NewAmount(cashu.Msat, fundsMSat), nil diff --git a/internal/lightning/fake_wallet.go b/internal/lightning/fake_wallet.go index 81ed8e5c..1776fd79 100644 --- a/internal/lightning/fake_wallet.go +++ b/internal/lightning/fake_wallet.go @@ -83,7 +83,6 @@ func (f FakeWallet) CheckPayed(quote string, invoice *zpay32.Invoice, checkingId return FAILED, "", cashu.Amount{Unit: cashu.Sat, Amount: 0}, nil case slices.Contains(f.UnpurposeErrors, FailQueryPending): return PENDING, "", cashu.Amount{Unit: cashu.Sat, Amount: 0}, nil - } return SETTLED, mock_preimage, cashu.Amount{Unit: cashu.Sat, Amount: 10}, nil @@ -97,7 +96,6 @@ func (f FakeWallet) CheckReceived(quote cashu.MintRequestDB, invoice *zpay32.Inv return FAILED, "", nil case slices.Contains(f.UnpurposeErrors, FailQueryPending): return PENDING, "", nil - } return SETTLED, mock_preimage, nil @@ -115,7 +113,7 @@ func (f FakeWallet) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mpp b return feesResponse, nil } -func (f FakeWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amount) (InvoiceResponse, error) { +func (f FakeWallet) RequestInvoice(amount cashu.Amount, description *string) (InvoiceResponse, error) { var response InvoiceResponse supported := f.VerifyUnitSupport(amount.Unit) if !supported { @@ -124,11 +122,11 @@ func (f FakeWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amoun expireTime := cashu.ExpiryTimeMinUnit(15) - description := "mock invoice" - if quote.Description != nil { - description = *quote.Description + invoiceDescription := "mock invoice" + if description != nil { + invoiceDescription = *description } - payReq, err := CreateMockInvoice(amount, description, f.Network, expireTime) + payReq, err := CreateMockInvoice(amount, invoiceDescription, f.Network, expireTime) if err != nil { return response, fmt.Errorf(`CreateMockInvoice(amount, "mock invoice", f.Network, expireTime). %w`, err) } diff --git a/internal/lightning/invoice.go b/internal/lightning/invoice.go index 28943469..a731e9f9 100644 --- a/internal/lightning/invoice.go +++ b/internal/lightning/invoice.go @@ -20,14 +20,12 @@ import ( // MPP invoice. func mockMppPaymentHashAndPreimage(d *invoicesrpc.AddInvoiceData) (*lntypes.Preimage, lntypes.Hash, error) { - var ( paymentPreimage *lntypes.Preimage paymentHash lntypes.Hash ) switch { - // Only either preimage or hash can be set. case d.Preimage != nil && d.Hash != nil: return nil, lntypes.Hash{}, @@ -60,7 +58,6 @@ func CreateMockInvoice(amountSats cashu.Amount, description string, network chai err := amountSats.To(cashu.Msat) if err != nil { return "", fmt.Errorf("amountSats.To(cashu.Msat): %w", err) - } milsats, err := lnrpc.UnmarshallAmt(0, int64(amountSats.Amount)) if err != nil { @@ -102,7 +99,6 @@ func CreateMockInvoice(amountSats cashu.Amount, description string, network chai if err != nil { return "", err - } payReqString, err := payReq.Encode(zpay32.MessageSigner{ diff --git a/internal/lightning/lightning_test.go b/internal/lightning/lightning_test.go index 4f92b4b5..a5d199b5 100644 --- a/internal/lightning/lightning_test.go +++ b/internal/lightning/lightning_test.go @@ -36,7 +36,6 @@ func TestUseMinimumFeeOnInvoice(t *testing.T) { } if feeRes.Fees.Amount != 100 { - t.Errorf(`Fee is not being set to the correct value. %v`, feeRes.Fees.Amount) } } @@ -69,7 +68,6 @@ func TestUseFeeInvoice(t *testing.T) { } if feeRes.Fees.Amount != 150 { - t.Errorf(`Fee is not being set to the correct value. %v`, feeRes.Fees) } } diff --git a/internal/lightning/lnbits.go b/internal/lightning/lnbits.go index 19177cdc..683715bd 100644 --- a/internal/lightning/lnbits.go +++ b/internal/lightning/lnbits.go @@ -25,8 +25,8 @@ type LnbitsWallet struct { } type LNBitsDetailErrorData struct { - Detail string - Status string + Detail string `json:"detail,omitempty"` + Status string `json:"status,omitempty"` } type lnbitsInvoiceRequest struct { Unit string `json:"unit,omitempty"` @@ -79,7 +79,8 @@ func (l *LnbitsWallet) LnbitsRequest(method string, endpoint string, reqBody any body, err := io.ReadAll(resp.Body) defer func() { - if err := resp.Body.Close(); err != nil { + err := resp.Body.Close() + if err != nil { slog.Warn("failed to close response body", slog.Any("error", err)) } }() @@ -249,7 +250,7 @@ func (l LnbitsWallet) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mpp return feesResponse, nil } -func (l LnbitsWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amount) (InvoiceResponse, error) { +func (l LnbitsWallet) RequestInvoice(amount cashu.Amount, description *string) (InvoiceResponse, error) { // Convert amount to Sat for LNBits amountSat := cashu.Amount{Unit: amount.Unit, Amount: amount.Amount} err := amountSat.To(cashu.Sat) @@ -267,8 +268,8 @@ func (l LnbitsWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amo CheckingId: "", } - if quote.Description != nil { - reqInvoice.Memo = *quote.Description + if description != nil { + reqInvoice.Memo = *description } var response InvoiceResponse @@ -289,7 +290,6 @@ func (l LnbitsWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amo } if lnbitsInvoice.Bolt11 != "" { - response.PaymentRequest = lnbitsInvoice.Bolt11 } else { response.PaymentRequest = lnbitsInvoice.PaymentRequest @@ -299,7 +299,6 @@ func (l LnbitsWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amo response.CheckingId = lnbitsInvoice.PaymentHash return response, nil - } func (l LnbitsWallet) WalletBalance() (cashu.Amount, error) { diff --git a/internal/lightning/lnd.go b/internal/lightning/lnd.go index 9e13a8cc..9173cf0c 100644 --- a/internal/lightning/lnd.go +++ b/internal/lightning/lnd.go @@ -166,7 +166,6 @@ func (l *LndGrpcWallet) lndGrpcPayPartialInvoice( totalAttempts := 50 var routes []*lnrpc.Route for i := 0; i < totalAttempts; i++ { - //nolint:exhaustruct queryRoutes := lnrpc.QueryRoutesRequest{ PubKey: hex.EncodeToString(zpayInvoice.Destination.SerializeCompressed()), @@ -237,12 +236,10 @@ func (l *LndGrpcWallet) lndGrpcPayPartialInvoice( return nil default: continue - } } } return fmt.Errorf("multi nut no route. %w", cashu.ErrPaymentNoRoute) - } func (l LndGrpcWallet) PayInvoice(melt_quote cashu.MeltRequestDB, zpayInvoice *zpay32.Invoice, feeReserve cashu.Amount, mpp bool, amount cashu.Amount) (PaymentResponse, error) { @@ -309,7 +306,6 @@ func (l LndGrpcWallet) getPaymentStatus(invoice *zpay32.Invoice) (LndPayStatus, return payStatus, nil default: continue - } } } @@ -359,7 +355,6 @@ func (l LndGrpcWallet) CheckReceived(quote cashu.MintRequestDB, invoice *zpay32. case lnrpc.Invoice_OPEN: return PENDING, hex.EncodeToString(invoiceStatus.RPreimage), nil - } return PENDING, "", nil } @@ -453,7 +448,7 @@ func (l LndGrpcWallet) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mp return feesResponse, nil } -func (l LndGrpcWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amount) (InvoiceResponse, error) { +func (l LndGrpcWallet) RequestInvoice(amount cashu.Amount, description *string) (InvoiceResponse, error) { var response InvoiceResponse supported := l.VerifyUnitSupport(amount.Unit) if !supported { @@ -469,13 +464,15 @@ func (l LndGrpcWallet) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Am return response, fmt.Errorf(`amount.To(cashu.Sat) %w`, err) } - Lndinvoice := lnrpc.Invoice{ValueMsat: int64(amount.Amount), Expiry: 900} //nolint:exhaustruct - if quote.Description != nil { - Lndinvoice.Memo = *quote.Description + var lndInvoice lnrpc.Invoice + lndInvoice.ValueMsat = int64(amount.Amount) + lndInvoice.Expiry = 900 + if description != nil { + lndInvoice.Memo = *description } // Expiry time is 15 minutes - res, err := client.AddInvoice(ctx, &Lndinvoice) + res, err := client.AddInvoice(ctx, &lndInvoice) if err != nil { return response, err diff --git a/internal/lightning/strike.go b/internal/lightning/strike.go index fb630ec5..93de901f 100644 --- a/internal/lightning/strike.go +++ b/internal/lightning/strike.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/btcsuite/btcd/chaincfg" + "github.com/google/uuid" "github.com/lescuer97/nutmix/api/cashu" "github.com/lightningnetwork/lnd/zpay32" ) @@ -90,10 +91,8 @@ func CashuAmountToStrikeAmount(amount cashu.Amount) (strikeAmount, error) { Amount: floatStr, Currency: EUR, }, nil - } return strikeAmt, cashu.ErrCouldNotConvertUnit - } type strikeInvoiceResponse struct { @@ -190,7 +189,6 @@ func (l *Strike) StrikeRequest(method string, endpoint string, reqBody any, resp err := resp.Body.Close() if err != nil { slog.Error("could not close body from call.", slog.Any("error", err)) - } }() @@ -221,9 +219,7 @@ func (l *Strike) StrikeRequest(method string, endpoint string, reqBody any, resp return fmt.Errorf("unauthorized %+v", errorBody) default: return fmt.Errorf("unknown error %+v", errorBody) - } - } } @@ -359,7 +355,7 @@ func (l Strike) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mpp bool, return FeesResponse, nil } -func (l Strike) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amount) (InvoiceResponse, error) { +func (l Strike) RequestInvoice(amount cashu.Amount, description *string) (InvoiceResponse, error) { var response InvoiceResponse supported := l.VerifyUnitSupport(amount.Unit) if !supported { @@ -371,8 +367,8 @@ func (l Strike) RequestInvoice(quote cashu.MintRequestDB, amount cashu.Amount) ( return response, fmt.Errorf("CashuAmountToStrikeAmount(amount): %w", err) } reqInvoice := strikeInvoiceRequest{ - CorrelationId: quote.Quote, - Description: quote.Description, + CorrelationId: uuid.New().String(), + Description: description, Amount: strikeAmt, } @@ -413,7 +409,6 @@ func (l Strike) WalletBalance() (cashu.Amount, error) { } balanceTotal += currentBalance.Amount } - } return cashu.Amount{Unit: cashu.Msat, Amount: balanceTotal * 1000}, nil diff --git a/internal/mint/auth.go b/internal/mint/auth.go index 1574f4d1..6d56dcbf 100644 --- a/internal/mint/auth.go +++ b/internal/mint/auth.go @@ -24,7 +24,8 @@ func (m *Mint) verifyClams(clams cashu.AuthClams) error { } defer func() { if err != nil { - if rollbackErr := m.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } } @@ -57,7 +58,6 @@ func (m *Mint) verifyClams(clams cashu.AuthClams) error { } return nil - } func (m *Mint) VerifyAuthClearToken(token string) error { @@ -103,7 +103,8 @@ func (m *Mint) VerifyAuthBlindToken(authProof cashu.AuthProof) error { } defer func() { if err != nil { - if rollbackErr := m.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } } @@ -133,7 +134,7 @@ func (m *Mint) VerifyAuthBlindToken(authProof cashu.AuthProof) error { err = m.MintDB.SetProofsState(tx, proofArray, cashu.PROOF_SPENT) if err != nil { - return fmt.Errorf("m.MintDB.GetProofsFromSecretCurve(tx, []string{y} ). %w", err) + return fmt.Errorf("m.MintDB.SetProofsState(tx, proofArray, cashu.PROOF_SPENT). %w", err) } err = m.MintDB.Commit(ctx, tx) diff --git a/internal/mint/bolt11.go b/internal/mint/bolt11.go index 9c21e24d..13e2ddbe 100644 --- a/internal/mint/bolt11.go +++ b/internal/mint/bolt11.go @@ -14,7 +14,6 @@ import ( ) func CheckMintRequest(mint *Mint, quote cashu.MintRequestDB, invoice *zpay32.Invoice) (cashu.MintRequestDB, error) { - status, _, err := mint.LightningBackend.CheckReceived(quote, invoice) if err != nil { return quote, fmt.Errorf("mint.VerifyLightingPaymentHappened(pool). %w", err) @@ -26,14 +25,11 @@ func CheckMintRequest(mint *Mint, quote cashu.MintRequestDB, invoice *zpay32.Inv // quote.State = cashu.PENDING case lightning.FAILED: quote.State = cashu.UNPAID - } return quote, nil - } func CheckMeltRequest(ctx context.Context, mint *Mint, quoteId string) (cashu.PostMeltQuoteBolt11Response, error) { - tx, err := mint.MintDB.GetTx(ctx) if err != nil { return cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) @@ -41,7 +37,8 @@ func CheckMeltRequest(ctx context.Context, mint *Mint, quoteId string) (cashu.Po defer func() { if err != nil { - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } } @@ -88,7 +85,6 @@ func CheckMeltRequest(ctx context.Context, mint *Mint, quoteId string) (cashu.Po quote.State = cashu.PENDING case lightning.FAILED: quote.State = cashu.UNPAID - } err = mint.MintDB.AddPreimageMeltRequest(tx, quote.Quote, preimage) @@ -102,5 +98,4 @@ func CheckMeltRequest(ctx context.Context, mint *Mint, quoteId string) (cashu.Po } return quote.GetPostMeltQuoteResponse(), nil - } diff --git a/internal/mint/info.go b/internal/mint/info.go new file mode 100644 index 00000000..ef12989f --- /dev/null +++ b/internal/mint/info.go @@ -0,0 +1,193 @@ +package mint + +import ( + "time" + + "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/utils" +) + +func (m *Mint) Info() cashu.GetInfoResponse { + contacts := []cashu.ContactInfo{} + + email := m.Config.EMAIL + + if len(email) > 0 { + contacts = append(contacts, cashu.ContactInfo{ + Method: "email", + Info: email, + }) + } + + nostr := m.Config.NOSTR + + if len(nostr) > 0 { + contacts = append(contacts, cashu.ContactInfo{ + Method: "nostr", + Info: nostr, + }) + } + + nuts := make(map[string]any) + var baseNuts = []string{"1", "2", "3", "4", "5", "6"} + + var optionalNuts = []string{"7", "8", "9", "10", "11", "12", "17", "20"} + + if m.LightningBackend.ActiveMPP() { + optionalNuts = append(optionalNuts, "15") + } + if m.Config.MINT_REQUIRE_AUTH { + optionalNuts = append(optionalNuts, "21") + optionalNuts = append(optionalNuts, "22") + } + + for _, nut := range baseNuts { + b := false + + switch nut { + case "4": + bolt11Method := cashu.SwapMintMethod{ + Method: cashu.MethodBolt11, + Unit: cashu.Sat.String(), + MinAmount: 0, + MaxAmount: 0, + Options: nil, + Commands: nil, + } + + if m.Config.PEG_IN_LIMIT_SATS != nil { + bolt11Method.MaxAmount = *m.Config.PEG_IN_LIMIT_SATS + } + + descriptionEnabled := m.LightningBackend.DescriptionSupport() + bolt11Method.Options = &cashu.SwapMintMethodOptions{ + Description: &descriptionEnabled, + } + + nuts[nut] = cashu.SwapMintInfo{ + Methods: &[]cashu.SwapMintMethod{ + bolt11Method, + }, + Disabled: &m.Config.PEG_OUT_ONLY, + Supported: nil, + } + case "5": + bolt11Method := cashu.SwapMintMethod{ + Method: cashu.MethodBolt11, + Unit: cashu.Sat.String(), + MinAmount: 0, + MaxAmount: 0, + Options: nil, + Commands: nil, + } + + if m.Config.PEG_OUT_LIMIT_SATS != nil { + bolt11Method.MaxAmount = *m.Config.PEG_OUT_LIMIT_SATS + } + + nuts[nut] = cashu.SwapMintInfo{ + Methods: &[]cashu.SwapMintMethod{ + bolt11Method, + }, + Disabled: &b, + Supported: nil, + } + + default: + nuts[nut] = cashu.SwapMintInfo{ + Disabled: &b, + Methods: nil, + Supported: nil, + } + } + } + + for _, nut := range optionalNuts { + b := true + switch nut { + case "15": + bolt11Method := cashu.SwapMintMethod{ + Method: cashu.MethodBolt11, + Unit: cashu.Sat.String(), + MinAmount: 0, + MaxAmount: 0, + Options: nil, + Commands: nil, + } + + nuts[nut] = cashu.SwapMintInfo{ + Methods: &[]cashu.SwapMintMethod{ + bolt11Method, + }, + Disabled: nil, + Supported: nil, + } + case "17": + + wsMethod := make(map[string][]cashu.SwapMintMethod) + + bolt11Method := cashu.SwapMintMethod{ + Method: cashu.MethodBolt11, + Unit: cashu.Sat.String(), + MinAmount: 0, + MaxAmount: 0, + Options: nil, + Commands: []cashu.SubscriptionKind{ + cashu.Bolt11MeltQuote, + cashu.Bolt11MintQuote, + cashu.ProofStateWs, + }, + } + wsMethod["supported"] = []cashu.SwapMintMethod{bolt11Method} + + nuts[nut] = wsMethod + + case "20": + wsMethod := make(map[string]bool) + + wsMethod["supported"] = true + + nuts[nut] = wsMethod + + case "21": + formatedDiscoveryUrl := m.Config.MINT_AUTH_OICD_URL + "/.well-known/openid-configuration" + protectedRoutes := cashu.Nut21Info{ + OpenIdDiscovery: formatedDiscoveryUrl, + ClientId: m.Config.MINT_AUTH_OICD_CLIENT_ID, + ProtectedRoutes: cashu.ConvertRouteListToProtectedRouteList(m.Config.MINT_AUTH_CLEAR_AUTH_URLS), + } + + nuts[nut] = protectedRoutes + case "22": + protectedRoutes := cashu.Nut22Info{ + BatMaxMint: m.Config.MINT_AUTH_MAX_BLIND_TOKENS, + ProtectedRoutes: cashu.ConvertRouteListToProtectedRouteList(m.Config.MINT_AUTH_BLIND_AUTH_URLS), + } + + nuts[nut] = protectedRoutes + + default: + nuts[nut] = cashu.SwapMintInfo{ + Supported: &b, + Methods: nil, + Disabled: nil, + } + } + } + + response := cashu.GetInfoResponse{ + Name: m.Config.NAME, + Version: "nutmix/" + utils.AppVersion, + Pubkey: m.MintPubkey, + Description: m.Config.DESCRIPTION, + DescriptionLong: m.Config.DESCRIPTION_LONG, + Motd: m.Config.MOTD, + Contact: contacts, + Nuts: nuts, + IconUrl: m.Config.IconUrl, + TosUrl: m.Config.TosUrl, + Time: time.Now().Unix(), + } + + return response +} diff --git a/internal/mint/interface.go b/internal/mint/interface.go new file mode 100644 index 00000000..216d4e00 --- /dev/null +++ b/internal/mint/interface.go @@ -0,0 +1,9 @@ +package mint + +type METHOD = string + +const ( + Bolt11 METHOD = "BOLT_11" + Bolt12 METHOD = "BOLT_12" + BTC METHOD = "BTC" +) diff --git a/internal/mint/melting.go b/internal/mint/melting.go index c1bf819a..0ef51feb 100644 --- a/internal/mint/melting.go +++ b/internal/mint/melting.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "time" "github.com/jackc/pgconn" "github.com/jackc/pgx/v5" @@ -14,6 +15,159 @@ import ( "github.com/lightningnetwork/lnd/zpay32" ) +func (m *Mint) MeltQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request, method METHOD) (cashu.MeltRequestDB, error) { + switch method { + case Bolt11: + response, err := m.bolt11CreateMelt(ctx, meltRequest) + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.bolt11CreateMelt(ctx, meltRequest). %w ", err) + } + return response, nil + + default: + return cashu.MeltRequestDB{}, cashu.ErrPaymentMethodNotSupported + } +} + +func (m *Mint) bolt11CreateMelt(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request) (cashu.MeltRequestDB, error) { + requestData, err := m.bolt11ValidateMeltRequestQuote(ctx, meltRequest) + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.bolt11ValidateMeltRequestQuote. %w ", err) + } + + dbRequest, err := m.bolt11CreateMeltRequest(ctx, meltRequest, requestData) + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.bolt11CreateMeltRequest. %w ", err) + } + return dbRequest, nil +} +func (m *Mint) bolt11CreateMeltRequest(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request, requestData bolt11MeltReqData) (cashu.MeltRequestDB, error) { + quoteId, err := utils.RandomHash() + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("utils.RandomHash(). %w", err) + } + + expireTime := cashu.ExpiryTimeMinUnit(15) + now := time.Now().Unix() + queryFee := uint64(0) + checkingId := quoteId + amountToSend := requestData.Amount + if !requestData.Internal { + feesResponse, err := m.LightningBackend.QueryFees(meltRequest.Request, requestData.invoice, requestData.Internal, requestData.Amount) + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.LightningBackend.QueryFees. %w", err) + } + checkingId = feesResponse.CheckingId + queryFee = feesResponse.Fees.Amount + amountToSend = feesResponse.AmountToSend + } + //FIXME: Add method + dbRequest := cashu.MeltRequestDB{ + Amount: amountToSend.Amount, + Quote: quoteId, + Request: meltRequest.Request, + Unit: requestData.Unit.String(), + Expiry: expireTime, + FeeReserve: (queryFee + 1), + State: cashu.UNPAID, + PaymentPreimage: "", + SeenAt: now, + Mpp: requestData.Mpp, + CheckingId: checkingId, + FeePaid: 0, + Melted: false, + } + + tx, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.GetTx. %w", err) + } + defer func() { + rollbackErr := m.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { + if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + + err = m.MintDB.SaveMeltRequest(tx, dbRequest) + + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.SaveMeltRequest(tx, dbRequest). %w", err) + } + + err = m.MintDB.Commit(ctx, tx) + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.Commit(ctx, tx). %w", err) + } + return dbRequest, nil +} + +type bolt11MeltReqData struct { + invoice *zpay32.Invoice + Amount cashu.Amount + Unit cashu.Unit + Internal bool + Mpp bool +} + +func (m *Mint) bolt11ValidateMeltRequestQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request) (bolt11MeltReqData, error) { + unit, err := cashu.UnitFromString(meltRequest.Unit) + if err != nil { + return bolt11MeltReqData{}, errors.Join(err, cashu.ErrUnitNotSupported) + } + supported := m.LightningBackend.VerifyUnitSupport(unit) + if !supported { + return bolt11MeltReqData{}, errors.Join(err, cashu.ErrUnitNotSupported) + } + invoice, err := zpay32.Decode(meltRequest.Request, m.LightningBackend.GetNetwork()) + if err != nil { + return bolt11MeltReqData{}, fmt.Errorf(" zpay32.Decode. %w ", err) + } + + if uint64(*invoice.MilliSat) == 0 { + return bolt11MeltReqData{}, cashu.ErrAmountlessInvoiceNotSupported + } + + if m.Config.PEG_OUT_LIMIT_SATS != nil { + if int64(*invoice.MilliSat) > (int64(*m.Config.PEG_OUT_LIMIT_SATS) * 1000) { + return bolt11MeltReqData{}, cashu.ErrAmountOutsideLimit + } + } + invoiceAmountMilisats := uint64(*invoice.MilliSat) + cashuAmount := cashu.NewAmount(cashu.Msat, invoiceAmountMilisats) + err = cashuAmount.To(unit) + if err != nil { + return bolt11MeltReqData{}, fmt.Errorf("cashuAmount.To. %w ", err) + } + isMpp := false + mppAmount := cashu.NewAmount(unit, meltRequest.IsMpp()) + + // if mpp is valid than change amount to mpp amount + if mppAmount.Amount != 0 { + if mppAmount.Amount > cashuAmount.Amount { + return bolt11MeltReqData{}, fmt.Errorf("mpp amount is bigger than the invoice") + } + isMpp = true + cashuAmount = mppAmount + if !m.LightningBackend.ActiveMPP() { + // TODO: Add error code multi path payments being not allowed + return bolt11MeltReqData{}, fmt.Errorf("mpp not supported") + } + } + isInternal, err := m.IsInternalTransaction(ctx, meltRequest.Request) + if err != nil { + return bolt11MeltReqData{}, fmt.Errorf("m.IsInternalTransaction(ctx, meltRequest.Request). %w", err) + } + + if isMpp && isInternal { + return bolt11MeltReqData{}, fmt.Errorf("mpp is not allowed") + } + + return bolt11MeltReqData{Internal: isInternal, Mpp: isMpp, Amount: cashuAmount, Unit: unit, invoice: invoice}, nil +} + func (m *Mint) settleIfInternalMelt(tx pgx.Tx, meltQuote cashu.MeltRequestDB) (cashu.MeltRequestDB, error) { mintRequest, err := m.MintDB.GetMintRequestByRequest(tx, meltQuote.Request) if err != nil { @@ -46,6 +200,10 @@ func (m *Mint) settleIfInternalMelt(tx pgx.Tx, meltQuote cashu.MeltRequestDB) (c mintRequest.State = cashu.PAID slog.Info(fmt.Sprintf("Settling bolt11 payment internally: %v. mintRequest: %v, %v, %v", meltQuote.Quote, mintRequest.Quote, meltQuote.Amount, meltQuote.Unit)) + err = m.MintDB.ChangeMintRequestState(tx, mintRequest.Quote, mintRequest.State, mintRequest.Minted) + if err != nil { + return meltQuote, fmt.Errorf("m.MintDB.ChangeMintRequestState(tx, mintRequest.Quote, mintRequest.State, mintRequest.Minted) %w", err) + } err = m.MintDB.ChangeMeltRequestState(tx, meltQuote.Quote, meltQuote.State, meltQuote.Melted, meltQuote.FeePaid) if err != nil { return meltQuote, fmt.Errorf("m.MintDB.ChangeMeltRequestState(tx, meltQuote.Quote, meltQuote.State, meltQuote.Melted, meltQuote.FeePaid) %w", err) @@ -61,7 +219,8 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, initialTx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, initialTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } @@ -87,7 +246,6 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M } if quote.State == cashu.PENDING { - err = m.VerifyUnitSupport(quote.Unit) if err != nil { return quote, fmt.Errorf("m.VerifyUnitSupport(quote.Unit). %w", err) @@ -106,6 +264,7 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M if status == lightning.SETTLED { quote.State = cashu.PAID + quote.Melted = true // Convert fee to quote's unit for storage quoteUnit, err := cashu.UnitFromString(quote.Unit) if err != nil { @@ -128,7 +287,8 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M return cashu.MeltRequestDB{}, fmt.Errorf("settleTx, err := m.MintDB.GetTx(ctx). %w", err) } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, settleTx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, settleTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } @@ -150,7 +310,6 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M overpaidFees := pending_proofs.Amount() - totalExpent if len(changeMessages) > 0 && overpaidFees > 0 { - var blindMessages []cashu.BlindedMessage for _, v := range changeMessages { blindMessages = append(blindMessages, cashu.BlindedMessage{Id: v.Id, B_: v.B_, Witness: "", Amount: 0}) @@ -169,12 +328,11 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M if err != nil { return quote, fmt.Errorf("m.MintDB.DeleteChangeByQuote(quote.Quote) %w", err) } - } err = m.MintDB.SetProofsState(settleTx, pending_proofs, cashu.PROOF_SPENT) if err != nil { - return quote, fmt.Errorf("m.MintDB.SetProofsState(pending_proofs, cashu.PROOF_SPENT) %w", err) + return quote, fmt.Errorf("m.MintDB.SetProofsState(settleTx, pending_proofs, cashu.PROOF_SPENT) %w", err) } err = m.MintDB.ChangeMeltRequestState(settleTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) @@ -190,7 +348,6 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M if err != nil { return quote, fmt.Errorf("m.MintDB.Commit(ctx, settleTx). %w", err) } - } if status == lightning.FAILED { quote.State = cashu.UNPAID @@ -199,7 +356,8 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, failedLnTx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, failedLnTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } @@ -227,7 +385,6 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M return quote, fmt.Errorf("m.MintDB.Commit(ctx, failedLnTx). %w", err) } } - } return quote, nil @@ -252,167 +409,179 @@ func (m *Mint) CheckPendingQuoteAndProofs() error { return nil } -func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request) (cashu.PostMeltQuoteBolt11Response, error) { +type bolt11MeltData struct { + Fee cashu.Amount + AmountProofs cashu.Amount + Unit cashu.Unit +} + +func (m *Mint) bolt11MeltValidate(meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB) (bolt11MeltData, error) { if len(meltRequest.Inputs) == 0 { - return cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("outputs are empty") + return bolt11MeltData{}, fmt.Errorf("inputs or outputs are empty") } - - quote, err := m.CheckMeltQuoteState(ctx, meltRequest.Quote) - if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("mint.CheckMeltQuoteState(ctx, quoteId): %w", err) + if quote.State == cashu.PENDING { + slog.Warn("Quote is pending") + return bolt11MeltData{}, cashu.ErrQuoteIsPending } - if quote.State != cashu.UNPAID { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w", cashu.ErrMeltAlreadyPaid) + if quote.Melted { + slog.Info("Quote already melted", slog.String(utils.LogExtraInfo, quote.Quote)) + return bolt11MeltData{}, cashu.ErrMeltAlreadyPaid } + proofsAmount := meltRequest.Inputs.Amount() + keysets, err := m.Signer.GetKeysets() if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.Signer.GetKeys(). %w", err) + return bolt11MeltData{}, err } - unit, err := m.CheckProofsAreSameUnit(meltRequest.Inputs, keysets.Keysets) + // check for needed amount of fees + fee, err := cashu.Fees(meltRequest.Inputs, keysets.Keysets) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w. m.CheckProofsAreSameUnit(meltRequest.Inputs): %w", cashu.ErrUnitNotSupported, err) + return bolt11MeltData{}, fmt.Errorf("cashu.Fees(request.Inputs, keysets.Keysets). %w", err) } - // check for needed amount of fees - fee, err := cashu.Fees(meltRequest.Inputs, keysets.Keysets) + if proofsAmount < (quote.Amount + quote.FeeReserve + uint64(fee)) { + slog.Info(fmt.Sprintf("Not enought proofs to expend. Needs: %v", quote.Amount)) + return bolt11MeltData{}, fmt.Errorf("%w", cashu.ErrNotEnoughtProofs) + } + + unit, err := cashu.UnitFromString(quote.Unit) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("cashu.Fees(meltRequest.Inputs, mint.Keysets[unit.String()]): %w", err) + return bolt11MeltData{}, fmt.Errorf("cashu.UnitFromString. %w", err) } - AmountProofs, SecretsList, err := utils.GetAndCalculateProofsValues(&meltRequest.Inputs) + // get unit from proofs + proofUnit, err := checkProofsAreSameUnit(meltRequest.Inputs, keysets.Keysets) if err != nil { - slog.Warn("utils.GetProofsValues(&meltRequest.Inputs)", slog.Any("error", err)) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("utils.GetAndCalculateProofsValues(&meltRequest.Inputs) %w", err) + return bolt11MeltData{}, fmt.Errorf("m.CheckProofsAreSameUnit(proofs, keysets.Keysets). %w", err) } - if AmountProofs < (quote.Amount + quote.FeeReserve + uint64(fee)) { - slog.Info(fmt.Sprintf("Not enought proofs to expend. Needs: %v", quote.Amount)) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w", cashu.ErrNotEnoughtProofs) + if len(meltRequest.Outputs) > 0 { + // check if outputs are + outputUnit, err := verifyOutputs(meltRequest.Outputs, keysets.Keysets) + if err != nil { + return bolt11MeltData{}, fmt.Errorf("m.VerifyOutputs(outputs). %w", err) + } + + if proofUnit != outputUnit { + return bolt11MeltData{}, fmt.Errorf("proofUnit != messageUnit. %w", cashu.ErrNotSameUnits) + } + } + // validate if the proofs are correctly signed + err = m.VerifyProofsBDHKE(meltRequest.Inputs) + if err != nil { + return bolt11MeltData{}, fmt.Errorf("m.VerifyProofsBDHKE(proofs). %w", err) } - // Verify spending conditions + // Verify spending conditions - EXCLUSIVE paths following CDK pattern hasSigAll, err := cashu.ProofsHaveSigAll(meltRequest.Inputs) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("cashu.ProofsHaveSigAll(meltRequest.Inputs) %w", err) + return bolt11MeltData{}, fmt.Errorf("cashu.ProofsHaveSigAll(inputs). %w", err) } if hasSigAll { // SIG_ALL path: verify all conditions match and signature is valid against combined message err = meltRequest.ValidateSigflag() if err != nil { - slog.Debug("meltRequest.ValidateSigflag()", slog.String(utils.LogExtraInfo, err.Error())) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("meltRequest.ValidateSigflag() %w", err) + return bolt11MeltData{}, fmt.Errorf("request.ValidateSigflag(). %w", err) } } else { // Individual verification path: verify each proof's P2PK/HTLC spend conditions err = cashu.VerifyProofsSpendConditions(meltRequest.Inputs) if err != nil { - slog.Debug("m.VerifyProofsSpendConditions(meltRequest.Inputs)", slog.String(utils.LogExtraInfo, err.Error())) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.VerifyProofsSpendConditions(meltRequest.Inputs) %w", err) + return bolt11MeltData{}, fmt.Errorf("cashu.VerifyProofsSpendConditions(request.Inputs). %w", err) } } - - // Always verify BDHKE cryptographic signatures (regardless of SIG_ALL) - err = m.VerifyProofsBDHKE(meltRequest.Inputs) - if err != nil { - slog.Debug("m.VerifyProofsBDHKE(meltRequest.Inputs)", slog.String(utils.LogExtraInfo, err.Error())) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.VerifyProofsBDHKE(meltRequest.Inputs) %w", err) + if unit != proofUnit { + return bolt11MeltData{}, fmt.Errorf("proofs unit are not the same as the quote. %w", cashu.ErrDifferentInputOutputUnit) } + return bolt11MeltData{Fee: cashu.NewAmount(proofUnit, uint64(fee)), Unit: unit, AmountProofs: cashu.NewAmount(proofUnit, proofsAmount)}, nil +} - preparationTx, err := m.MintDB.GetTx(ctx) +func (m *Mint) validateMeltStatusAndSpent(ctx context.Context, meltRequest cashu.PostMeltBolt11Request) (cashu.MeltRequestDB, error) { + // check if proofs are spent and if outputs are spent + sizeCheckTx, err := m.MintDB.GetTx(ctx) if err != nil { - return cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, preparationTx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + if err != nil { + rollbackErr := m.MintDB.Rollback(ctx, sizeCheckTx) + if rollbackErr != nil { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } } }() - - quote, err = m.MintDB.GetMeltRequestById(preparationTx, meltRequest.Quote) - if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.GetMeltRequestById(preparationTx, meltRequest.Quote): %w", err) - } - err = m.VerifyUnitSupport(quote.Unit) + quote, err := m.MintDB.GetMeltRequestById(sizeCheckTx, meltRequest.Quote) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.VerifyUnitSupport(quote.Unit). %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.GetMeltRequestById(preparationTx, meltRequest.Quote): %w", err) } - if quote.State == cashu.PENDING { slog.Warn("Quote is pending") - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w", cashu.ErrQuoteIsPending) + return cashu.MeltRequestDB{}, cashu.ErrQuoteIsPending } if quote.Melted { slog.Info("Quote already melted", slog.String(utils.LogExtraInfo, quote.Quote)) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w", cashu.ErrMeltAlreadyPaid) + return cashu.MeltRequestDB{}, cashu.ErrMeltAlreadyPaid } - // check if we know any of the proofs - knownProofs, err := m.MintDB.GetProofsFromSecretCurve(preparationTx, SecretsList) + proofs, err := m.checkProofSpent(sizeCheckTx, meltRequest.Inputs) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.GetProofsFromSecretCurve(preparationTx, SecretsList) %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.checkProofSpent(sizeCheckTx, request.Inputs). %w", err) } - if len(knownProofs) != 0 { - slog.Debug("Proofs already used", slog.String(utils.LogExtraInfo, fmt.Sprintf("knownproofs: %+v", knownProofs))) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w: len(knownProofs) != 0", cashu.ErrProofSpent) - } - if len(meltRequest.Outputs) > 0 { - outputUnit, err := m.VerifyOutputs(preparationTx, meltRequest.Outputs, keysets.Keysets) - if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w. m.VerifyOutputs(meltRequest.Outputs): %w", cashu.ErrUnitNotSupported, err) - } - - if outputUnit != unit { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("%w. Change output unit is different: ", cashu.ErrDifferentInputOutputUnit) - } + err = m.CheckOutputSpent(sizeCheckTx, meltRequest.Outputs) + if err != nil { + return cashu.MeltRequestDB{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) } - // change state to pending - meltRequest.Inputs.SetPendingAndQuoteRef(quote.Quote) - quote.State = cashu.PENDING - err = m.MintDB.SaveProof(preparationTx, meltRequest.Inputs) + proofs.SetPendingAndQuoteRef(quote.Quote) + err = m.MintDB.SaveProof(sizeCheckTx, proofs) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.SaveProof(preparationTx, meltRequest.Inputs) %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.SaveProof(sizeCheckTx, proofs). %w", err) } - err = m.MintDB.ChangeMeltRequestState(preparationTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) + + quote.State = cashu.PENDING + err = m.MintDB.ChangeMeltRequestState(sizeCheckTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.ChangeMeltRequestState(preparationTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.ChangeMeltRequestState(preparationTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) %w", err) } - err = m.MintDB.SaveMeltChange(preparationTx, meltRequest.Outputs, quote.Quote) + err = m.MintDB.SaveMeltChange(sizeCheckTx, meltRequest.Outputs, quote.Quote) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.SaveMeltChange(setUpTx, meltRequest.Outputs, quote.Quote) %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.SaveMeltChange(setUpTx, meltRequest.Outputs, quote.Quote) %w", err) } - quote, err = m.settleIfInternalMelt(preparationTx, quote) + quote, err = m.settleIfInternalMelt(sizeCheckTx, quote) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.settleIfInternalMelt(ctx, preparationTx, quote). %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.settleIfInternalMelt(ctx, preparationTx, quote). %w", err) } - err = m.MintDB.Commit(ctx, preparationTx) + err = m.MintDB.Commit(ctx, sizeCheckTx) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.Commit(ctx, preparationTx). %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.Commit(ctx, sizeCheckTx). %w", err) } + return quote, nil +} +func (m *Mint) bolt11PayInvoice(ctx context.Context, meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB) (cashu.MeltRequestDB, cashu.Amount, error) { // Commit all blind messages and proofs as pending before going over the network invoice, err := zpay32.Decode(quote.Request, m.LightningBackend.GetNetwork()) if err != nil { slog.Info(fmt.Errorf("zpay32.Decode: %w", err).Error()) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("zpay32.Decode(quote.Request, m.LightningBackend.GetNetwork()) %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("zpay32.Decode(quote.Request, m.LightningBackend.GetNetwork()) %w", err) } - var paidLightningFeeSat uint64 + unit, err := cashu.UnitFromString(quote.Unit) + if err != nil { + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("cashu.UnitFromString(quote.Unit). %w", err) + } amount := cashu.NewAmount(unit, quote.Amount) if quote.State != cashu.PAID { - // Convert feeReserve to Amount for the lightning backend feeReserveAmount := cashu.NewAmount(unit, quote.FeeReserve) payment, err := m.LightningBackend.PayInvoice(quote, invoice, feeReserveAmount, quote.Mpp, amount) @@ -420,10 +589,11 @@ func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request if err != nil || payment.PaymentState == lightning.FAILED || payment.PaymentState == lightning.UNKNOWN || payment.PaymentState == lightning.PENDING { lnTx, err := m.MintDB.GetTx(ctx) if err != nil { - return cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err) } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, lnTx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, lnTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } @@ -436,11 +606,11 @@ func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request quote.CheckingId = payment.CheckingId err = m.MintDB.ChangeCheckingId(lnTx, quote.Quote, quote.CheckingId) if err != nil { - slog.Error(fmt.Errorf("m.MintDB.ChangeCheckingId(lnTx, quote.Quote, quote.CheckingId): %w", err).Error()) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.MintDB.ChangeCheckingId(lnTx, quote.Quote, quote.CheckingId): %w", err) } err = m.MintDB.Commit(ctx, lnTx) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.Commit(ctx, lnTx). %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.MintDB.Commit(ctx, lnTx). %w", err) } // if exception of lightning payment says fail do a payment status recheck. @@ -449,27 +619,24 @@ func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request // if error on checking payement we will save as pending and returns status if err != nil { slog.Warn("Something happened while paying the invoice. Keeping proofs and quote as pending ") - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.LightningBackend.CheckPayed(quote.Quote) %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.LightningBackend.CheckPayed(quote.Quote) %w", err) } slog.Info("after check paid verification") // Convert fee Amount to quote's unit for storage - quoteUnit, err := cashu.UnitFromString(quote.Unit) - if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("cashu.UnitFromString(quote.Unit). %w", err) - } - convertErr := fee_paid.To(quoteUnit) + convertErr := fee_paid.To(unit) if convertErr != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("fee_paid.To(quoteUnit). %w", convertErr) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("fee_paid.To(quoteUnit). %w", convertErr) } quote.FeePaid = fee_paid.Amount lnStatusTx, err := m.MintDB.GetTx(ctx) if err != nil { - return cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err) } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, lnStatusTx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, lnStatusTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } @@ -483,7 +650,7 @@ func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request // change melt request state err = m.MintDB.ChangeMeltRequestState(lnStatusTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) if err != nil { - slog.Error(fmt.Errorf("m.MintDB.ChangeMeltRequestState(lnStatusTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid): %w", err).Error()) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.MintDB.ChangeMeltRequestState(lnStatusTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid): %w", err) } // finish failure and release the proofs @@ -491,113 +658,163 @@ func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request quote.State = cashu.UNPAID errDb := m.MintDB.ChangeMeltRequestState(lnStatusTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) if errDb != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.ChangeMeltRequestState(lnStatusTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.MintDB.ChangeMeltRequestState(lnStatusTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) %w", err) } errDb = m.MintDB.DeleteProofs(lnStatusTx, meltRequest.Inputs) if errDb != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.DeleteProofs(lnStatusTx, meltRequest.Inputs) %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.MintDB.DeleteProofs(lnStatusTx, meltRequest.Inputs) %w", err) } errDb = m.MintDB.DeleteChangeByQuote(lnStatusTx, quote.Quote) if errDb != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.DeleteChangeByQuote(lnStatusTx, quote.Quote) %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.MintDB.DeleteChangeByQuote(lnStatusTx, quote.Quote) %w", err) } } err = m.MintDB.Commit(ctx, lnStatusTx) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.Commit(ctx, lnStatusTx). %w", err) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("m.MintDB.Commit(ctx, lnStatusTx). %w", err) } - return quote.GetPostMeltQuoteResponse(), nil + return quote, cashu.Amount{Amount: 0, Unit: unit}, nil } + quote.PaymentPreimage = payment.Preimage // Convert fee Amount to quote's unit for storage - quoteUnit, err := cashu.UnitFromString(quote.Unit) - if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("cashu.UnitFromString(quote.Unit). %w", err) - } - convertErr := payment.PaidFee.To(quoteUnit) + convertErr := payment.PaidFee.To(unit) if convertErr != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("payment.PaidFee.To(quoteUnit). %w", convertErr) + return cashu.MeltRequestDB{}, cashu.Amount{}, fmt.Errorf("payment.PaidFee.To(quoteUnit). %w", convertErr) } quote.FeePaid = payment.PaidFee.Amount quote.State = cashu.PAID quote.Melted = true + return quote, payment.PaidFee, nil } + return quote, cashu.Amount{Amount: 0, Unit: unit}, nil +} - response := quote.GetPostMeltQuoteResponse() +func (m *Mint) bolt11MeltBurnTokens(ctx context.Context, meltData bolt11MeltData, meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB, paidLightningFeeSat cashu.Amount) (cashu.MeltRequestDB, cashu.PostMeltQuoteBolt11Response, cashu.Proofs, error) { + err := paidLightningFeeSat.To(meltData.Unit) + if err != nil { + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("paidLightningFeeSat.To(meltData) %w", err) + } + err = meltData.Fee.To(meltData.Unit) + if err != nil { + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("meltData.Fee.To(meltData) %w", err) + } - // if fees where lower than expected return sats to the user + totalExpent := quote.Amount + paidLightningFeeSat.Amount + meltData.Fee.Amount + + var recoverySigs []cashu.RecoverSigDB + var blindSigs []cashu.BlindSignature + if meltData.AmountProofs.Amount > totalExpent && len(meltRequest.Outputs) > 0 { + overpaidFees := meltData.AmountProofs.Amount - totalExpent + change := utils.GetMessagesForChange(overpaidFees, meltRequest.Outputs) + + blindSignaturesDB, recoverySigsDb, err := m.Signer.SignBlindMessages(change) + if err != nil { + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("m.Signer.SignBlindMessages(change) %w", err) + } + recoverySigs = recoverySigsDb + blindSigs = blindSignaturesDB + } - // if total expent is lower that the amount of proofs that where given - // change is returned paidLnxTx, err := m.MintDB.GetTx(ctx) if err != nil { - return cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err) } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, paidLnxTx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, paidLnxTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } } }() - totalExpent := quote.Amount + paidLightningFeeSat + uint64(fee) - if AmountProofs > totalExpent && len(meltRequest.Outputs) > 0 { - overpaidFees := AmountProofs - totalExpent - change := utils.GetMessagesForChange(overpaidFees, meltRequest.Outputs) - - blindSignatures, recoverySigsDb, err := m.Signer.SignBlindMessages(change) - - if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.Signer.SignBlindMessages(change) %w", err) - } - - err = m.MintDB.SaveRestoreSigs(paidLnxTx, recoverySigsDb) + response := quote.GetPostMeltQuoteResponse() + if len(recoverySigs) > 0 { + err = m.MintDB.SaveRestoreSigs(paidLnxTx, recoverySigs) if err != nil { - slog.Error("recoverySigsDb", slog.String(utils.LogExtraInfo, fmt.Sprintf("%+v", recoverySigsDb))) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.SaveRestoreSigs(paidLnxTx, recoverySigsDb) %w", err) - + slog.Error("recoverySigsDb", slog.String(utils.LogExtraInfo, fmt.Sprintf("%+v", recoverySigs))) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("m.MintDB.SaveRestoreSigs(paidLnxTx, recoverySigsDb) %w", err) } err = m.MintDB.DeleteChangeByQuote(paidLnxTx, quote.Quote) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.DeleteChangeByQuote(paidLnxTx, quote.Quote) %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("m.MintDB.DeleteChangeByQuote(paidLnxTx, quote.Quote) %w", err) } - - response.Change = blindSignatures + response.Change = blindSigs } err = m.MintDB.ChangeMeltRequestState(paidLnxTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.ChangeMeltRequestState(paidLnxTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("m.MintDB.ChangeMeltRequestState(paidLnxTx, quote.Quote, quote.State, quote.Melted, quote.FeePaid) %w", err) } err = m.MintDB.AddPreimageMeltRequest(paidLnxTx, quote.Quote, quote.PaymentPreimage) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.AddPreimageMeltRequest(paidLnxTx, quote.Quote, quote.PaymentPreimage) %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("m.MintDB.AddPreimageMeltRequest(paidLnxTx, quote.Quote, quote.PaymentPreimage) %w", err) } - // change proofs to spent meltRequest.Inputs.SetProofsState(cashu.PROOF_SPENT) - // send proofs to database err = m.MintDB.SetProofsState(paidLnxTx, meltRequest.Inputs, cashu.PROOF_SPENT) if err != nil { slog.Error("Proofs", slog.Any("proofs", meltRequest.Inputs)) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.SetProofsState(tx, meltRequest.Inputs, cashu.PROOF_SPENT) %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("m.MintDB.SetProofsState(tx, meltRequest.Inputs, cashu.PROOF_SPENT) %w", err) + } + + err = m.MintDB.Commit(ctx, paidLnxTx) + if err != nil { + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("m.MintDB.Commit(ctx, paidLnxTx). %w", err) + } + return quote, response, meltRequest.Inputs, nil +} +func (m *Mint) bolt11Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request) (cashu.MeltRequestDB, cashu.PostMeltQuoteBolt11Response, error) { + quote, err := m.CheckMeltQuoteState(ctx, meltRequest.Quote) + if err != nil { + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("mint.CheckMeltQuoteState(ctx, quoteId): %w", err) } - err = m.MintDB.DeleteChangeByQuote(paidLnxTx, quote.Quote) + meltRequestData, err := m.bolt11MeltValidate(meltRequest, quote) if err != nil { - slog.Info("mint.MintDB.SaveMeltChange(meltRequest.Outputs, quote.Quote)", slog.Any("error", err)) - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.DeleteChangeByQuote(tx, quote.Quote) %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.bolt11MeltValidate(meltRequest, quote): %w", err) + } + quote, err = m.validateMeltStatusAndSpent(ctx, meltRequest) + if err != nil { + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.validateMeltStatusAndSpent(ctx, meltRequest): %w", err) } - err = m.MintDB.Commit(ctx, paidLnxTx) + quote, lnFee, err := m.bolt11PayInvoice(ctx, meltRequest, quote) if err != nil { - return quote.GetPostMeltQuoteResponse(), fmt.Errorf("m.MintDB.Commit(ctx, paidLnxTx). %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.bolt11PayInvoice(ctx, meltRequest, quote): %w", err) + } + + if quote.State == cashu.PAID { + quote, response, spentProofs, err := m.bolt11MeltBurnTokens(ctx, meltRequestData, meltRequest, quote, lnFee) + if err != nil { + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.bolt11MeltBurnTokens(ctx, meltRequestData, meltRequest, quote, lnFee): %w", err) + } + + go m.Observer.SendProofsEvent(spentProofs) + go m.Observer.SendMeltEvent(quote) + + return quote, response, nil } go m.Observer.SendProofsEvent(meltRequest.Inputs) go m.Observer.SendMeltEvent(quote) - return response, nil + return quote, quote.GetPostMeltQuoteResponse(), nil +} + +func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request, method METHOD) (cashu.PostMeltQuoteBolt11Response, error) { + switch method { + case Bolt11: + _, response, err := m.bolt11Melt(ctx, meltRequest) + if err != nil { + return cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.bolt11Melt. %w ", err) + } + return response, nil + + default: + + return cashu.PostMeltQuoteBolt11Response{}, cashu.ErrPaymentMethodNotSupported + } } diff --git a/internal/mint/mint.go b/internal/mint/mint.go index c68ff5a8..0dee9c65 100644 --- a/internal/mint/mint.go +++ b/internal/mint/mint.go @@ -32,8 +32,7 @@ var ( MINT_LIGHTNING_BACKEND_ENV = "MINT_LIGHTNING_BACKEND" ) -func (m *Mint) CheckProofsAreSameUnit(proofs []cashu.Proof, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { - +func checkProofsAreSameUnit(proofs []cashu.Proof, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { units := make(map[string]bool) seenKeys := make(map[string]cashu.BasicKeysetResponse) @@ -67,7 +66,10 @@ func (m *Mint) CheckProofsAreSameUnit(proofs []cashu.Proof, keys []cashu.BasicKe } return returnedUnit, nil +} +func (m *Mint) CheckProofsAreSameUnit(proofs []cashu.Proof, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { + return checkProofsAreSameUnit(proofs, keys) } func CheckChainParams(network string) (chaincfg.Params, error) { @@ -85,7 +87,6 @@ func CheckChainParams(network string) (chaincfg.Params, error) { default: return chaincfg.MainNetParams, fmt.Errorf("invalid network: %s", network) } - } func SetUpMint(ctx context.Context, config utils.Config, nostrNotificationConfig *utils.NostrNotificationConfig, db database.MintDB, sig signer.Signer) (*Mint, error) { @@ -106,7 +107,6 @@ func SetUpMint(ctx context.Context, config utils.Config, nostrNotificationConfig } switch config.MINT_LIGHTNING_BACKEND { - case utils.FAKE_WALLET: fake_wallet := lightning.FakeWallet{ Network: chainparam, diff --git a/internal/mint/mint_test.go b/internal/mint/mint_test.go index 9776d83d..aeba1d92 100644 --- a/internal/mint/mint_test.go +++ b/internal/mint/mint_test.go @@ -78,7 +78,6 @@ func SetupMintWithLightningMockPostgres(t *testing.T) *Mint { } return mint - } const quoteId = "quoteid" @@ -194,6 +193,9 @@ func TestPendingQuotesAndProofsWithPostgresAndMockLNSuccess(t *testing.T) { if meltRequest.State != cashu.PAID { t.Errorf("State should be paid: %+v ", meltRequest.State) } + if !meltRequest.Melted { + t.Errorf("melt request should be marked melted: %+v ", meltRequest) + } ctx := context.Background() tx, err := mint.MintDB.GetTx(ctx) @@ -214,6 +216,9 @@ func TestPendingQuotesAndProofsWithPostgresAndMockLNSuccess(t *testing.T) { if savedQuote.State != cashu.PAID { t.Errorf("melt quote id: %+v ", meltRequest.Quote) } + if !savedQuote.Melted { + t.Errorf("saved quote should be marked melted: %+v ", savedQuote) + } meltChange, err := mint.MintDB.GetMeltChangeByQuote(tx, meltRequest.Quote) if err != nil { @@ -244,7 +249,6 @@ func TestPendingQuotesAndProofsWithPostgresAndMockLNSuccess(t *testing.T) { if err != nil { t.Fatalf("mint.MintDB.Commit(ctx, tx): %+v ", err) } - } func TestPendingQuotesAndProofsWithPostgresAndMockLNFail(t *testing.T) { mint := SetupMintWithLightningMockPostgres(t) diff --git a/internal/mint/minting.go b/internal/mint/minting.go new file mode 100644 index 00000000..6d879c7d --- /dev/null +++ b/internal/mint/minting.go @@ -0,0 +1,392 @@ +package mint + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/jackc/pgx/v5" + "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/lightning" + "github.com/lescuer97/nutmix/internal/utils" + "github.com/lightningnetwork/lnd/zpay32" +) + +func (m *Mint) CreateMintQuote(ctx context.Context, request cashu.PostMintQuoteBolt11Request, method METHOD) (cashu.PostMintQuoteBolt11Response, error) { + unit, err := m.validateMintConfiguration(request) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf("m.validateMintConfiguration(). %w", err) + } + switch method { + case Bolt11: + supported := m.LightningBackend.VerifyUnitSupport(unit) + if !supported { + return cashu.PostMintQuoteBolt11Response{}, errors.Join(err, cashu.ErrUnitNotSupported) + } + response, err := m.bolt11GenerateMintQuote(ctx, request, unit) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf("m.generateBolt11MintRequest(request,unit). %w", err) + } + return response, nil + + default: + return cashu.PostMintQuoteBolt11Response{}, cashu.ErrPaymentMethodNotSupported + } +} +func (m *Mint) validateMintConfiguration(request cashu.PostMintQuoteBolt11Request) (cashu.Unit, error) { + if request.Amount == 0 { + return cashu.Sat, fmt.Errorf("amount empty") + } + + if m.Config.PEG_OUT_ONLY { + return cashu.Sat, cashu.ErrMintintDisabled + } + + if m.Config.PEG_IN_LIMIT_SATS != nil { + if request.Amount > uint64(*m.Config.PEG_IN_LIMIT_SATS) { + slog.Info("Mint amount over the limit", slog.Uint64("amount", request.Amount)) + + return cashu.Sat, cashu.ErrAmountOutsideLimit + } + } + + unit, err := cashu.UnitFromString(request.Unit) + if err != nil { + return cashu.Sat, errors.Join(err, cashu.ErrUnitNotSupported) + } + + return unit, nil +} + +func (m *Mint) bolt11GenerateMintQuote(ctx context.Context, request cashu.PostMintQuoteBolt11Request, unit cashu.Unit) (cashu.PostMintQuoteBolt11Response, error) { + resInvoice, err := m.LightningBackend.RequestInvoice(cashu.NewAmount(unit, request.Amount), request.Description) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.LightningBackend.RequestInvoice. %w", err) + } + quoteId, err := utils.RandomHash() + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" utils.RandomHash() %w ", err) + } + + expireTime := cashu.ExpiryTimeMinUnit(15) + now := time.Now().Unix() + + mintRequestDB := cashu.MintRequestDB{ + Quote: quoteId, + Expiry: expireTime, + Unit: unit.String(), + State: cashu.UNPAID, + SeenAt: now, + Amount: &request.Amount, + Pubkey: request.Pubkey, + Description: request.Description, + Request: resInvoice.PaymentRequest, + CheckingId: resInvoice.CheckingId, + Minted: false, + } + tx, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.MintDB.GetTx(ctx). %w", err) + } + defer func() { + rollbackErr := m.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { + if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + + err = m.MintDB.SaveMintRequest(tx, mintRequestDB) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.MintDB.SaveMintRequest(tx, mintRequestDB). %w", err) + } + + err = m.MintDB.Commit(ctx, tx) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.MintDB.Commit(ctx, tx). %w", err) + } + + return mintRequestDB.PostMintQuoteBolt11Response(), nil +} + +// FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration +func (m *Mint) MintQuoteStatus(ctx context.Context, quoteId string, method METHOD) (cashu.PostMintQuoteBolt11Response, error) { + tx, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.MintDB.GetTx(ctx). %w", err) + } + defer func() { + rollbackErr := m.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { + if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + quote, err := m.MintDB.GetMintRequestById(tx, quoteId) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.MintDB.GetMintRequestById(tx, quoteId). %w", err) + } + err = m.MintDB.Commit(ctx, tx) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.MintDB.Commit(ctx, tx). %w", err) + } + switch method { + case Bolt11: + if quote.State == cashu.PAID || quote.State == cashu.ISSUED { + return quote.PostMintQuoteBolt11Response(), nil + } + bolt11Quote, err := m.bolt11CheckQuote(ctx, quote, method) + if err != nil { + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf("m.bolt11CheckQuote(ctx, quote, method). %w", err) + } + return bolt11Quote.PostMintQuoteBolt11Response(), nil + + default: + return cashu.PostMintQuoteBolt11Response{}, cashu.ErrPaymentMethodNotSupported + } +} + +// FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration +func (m *Mint) bolt11CheckQuote(ctx context.Context, request cashu.MintRequestDB, method METHOD) (cashu.MintRequestDB, error) { + if method != Bolt11 { + return cashu.MintRequestDB{}, fmt.Errorf("request method is not BOLT11") + } + invoice, err := zpay32.Decode(request.Request, m.LightningBackend.GetNetwork()) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("zpay32.Decode(request.Request, m.LightningBackend.GetNetwork()). %w", err) + } + + status, _, err := m.LightningBackend.CheckReceived(request, invoice) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("mint.VerifyLightingPaymentHappened(pool). %w", err) + } + stateChangeTX, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf(" m.MintDB.GetTx(ctx). %w", err) + } + defer func() { + rollbackErr := m.MintDB.Rollback(ctx, stateChangeTX) + if rollbackErr != nil { + if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + + switch status { + case lightning.SETTLED: + err = m.MintDB.ChangeMintRequestState(stateChangeTX, request.Quote, cashu.PAID, request.Minted) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.MintDB.ChangeMintRequestState(stateChangeTX, request.Quote, cashu.PAID, request.Minted). %w", err) + } + case lightning.PENDING: + // quote.State = cashu.PENDING + case lightning.FAILED: + err = m.MintDB.ChangeMintRequestState(stateChangeTX, request.Quote, cashu.UNPAID, request.Minted) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.MintDB.ChangeMintRequestState(stateChangeTX, request.Quote, cashu.UNPAID, request.Minted). %w", err) + } + } + + quote, err := m.MintDB.GetMintRequestById(stateChangeTX, request.Quote) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.MintDB.GetMintRequestById(stateChangeTX, request.Quote). %w", err) + } + err = m.MintDB.Commit(ctx, stateChangeTX) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf(" m.MintDB.Commit(ctx, tx). %w", err) + } + + return quote, nil +} + +func (m *Mint) Mint(ctx context.Context, request cashu.PostMintBolt11Request, method METHOD) (cashu.PostMintBolt11Response, error) { + mintReq, err := m.mintRequestValidate(ctx, request) + if err != nil { + return cashu.PostMintBolt11Response{}, fmt.Errorf(" mintRequestValidate(ctx, request). %w", err) + } + switch method { + case Bolt11: + response, err := m.bolt11Mint(ctx, request, mintReq, method) + if err != nil { + return cashu.PostMintBolt11Response{}, fmt.Errorf("m.bolt11Mint. %w", err) + } + return response, nil + + default: + return cashu.PostMintBolt11Response{}, cashu.ErrPaymentMethodNotSupported + } +} + +// takes the general values of the minting process and analyses them even before going to the method branching. +func (m *Mint) mintRequestValidate(ctx context.Context, request cashu.PostMintBolt11Request) (cashu.MintRequestDB, error) { + preparationTx, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf(" m.MintDB.GetTx(ctx). %w", err) + } + defer func() { + rollbackErr := m.MintDB.Rollback(ctx, preparationTx) + if rollbackErr != nil { + if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + quote, err := m.MintDB.GetMintRequestById(preparationTx, request.Quote) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.MintDB.GetMintRequestById(preparationTx, request.Quote). %w", err) + } + err = m.MintDB.Commit(ctx, preparationTx) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf(" m.MintDB.Commit(ctx, tx). %w", err) + } + err = m.validateMintStatusAndAuth(request, quote) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf(" m.bolt11ValidateMint(ctx, request, quote). %w", err) + } + keysets, err := m.Signer.GetKeysets() + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.Signer.GetKeysets(). %w", err) + } + + outputUnit, err := verifyOutputs(request.Outputs, keysets.Keysets) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("verifyOutputs(request.Outputs, keysets.Keysets). %w", err) + } + + if outputUnit.String() != quote.Unit { + return cashu.MintRequestDB{}, cashu.ErrDifferentInputOutputUnit + } + + // check if proofs are spent and if outputs are spent + sizeCheckTx, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) + } + defer func() { + if err != nil { + rollbackErr := m.MintDB.Rollback(ctx, sizeCheckTx) + if rollbackErr != nil { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + err = m.CheckOutputSpent(sizeCheckTx, request.Outputs) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + } + err = m.MintDB.Commit(ctx, sizeCheckTx) + if err != nil { + return cashu.MintRequestDB{}, fmt.Errorf("m.MintDB.Commit(ctx, sizeCheckTx). %w", err) + } + return quote, nil +} + +// FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration +func (m *Mint) bolt11Mint(ctx context.Context, request cashu.PostMintBolt11Request, mintReq cashu.MintRequestDB, method METHOD) (cashu.PostMintBolt11Response, error) { + if method != Bolt11 { + return cashu.PostMintBolt11Response{}, fmt.Errorf("request method is not BOLT11") + } + + unit, err := cashu.UnitFromString(mintReq.Unit) + if err != nil { + return cashu.PostMintBolt11Response{}, fmt.Errorf("cashu.UnitFromString(mintReq.Unit) %w", err) + } + + supported := m.LightningBackend.VerifyUnitSupport(unit) + if !supported { + return cashu.PostMintBolt11Response{}, fmt.Errorf(" m.LightningBackend.VerifyUnitSupport(unit). %w. %w", err, cashu.ErrUnitNotSupported) + } + + invoice, err := zpay32.Decode(mintReq.Request, m.LightningBackend.GetNetwork()) + if err != nil { + return cashu.PostMintBolt11Response{}, fmt.Errorf("zpay32.Decode(mintRequestDB.Request, mint.LightningBackend.GetNetwork()). %w", err) + } + cashuBlindMessage := cashu.NewAmount(unit, request.Outputs.Amount()) + err = cashuBlindMessage.To(cashu.Msat) + if err != nil { + return cashu.PostMintBolt11Response{}, err + } + + // Mint outputs must match the invoice amount exactly. + if uint64(*invoice.MilliSat) != cashuBlindMessage.Amount { + slog.Info("mismatched amount of milisats", slog.Int("invoice_milisats", int(*invoice.MilliSat)), slog.Int("requested_milisats", int(cashuBlindMessage.Amount))) + return cashu.PostMintBolt11Response{}, cashu.ErrAmountNotEqualToInvoice + } + + if mintReq.State != cashu.PAID { + mintReq, err = m.bolt11CheckQuote(ctx, mintReq, method) + if err != nil { + return cashu.PostMintBolt11Response{}, fmt.Errorf("m.bolt11CheckQuote(ctx, quote, method). %w", err) + } + } + + if mintReq.State != cashu.PAID { + return cashu.PostMintBolt11Response{}, cashu.ErrRequestNotPaid + } + + blindSigs, err := m.signAndSaveSigs(ctx, request, mintReq) + if err != nil { + return cashu.PostMintBolt11Response{}, err + } + return cashu.PostMintBolt11Response{Signatures: blindSigs}, nil +} + +func (m *Mint) signAndSaveSigs(ctx context.Context, request cashu.PostMintBolt11Request, mintRequestDB cashu.MintRequestDB) ([]cashu.BlindSignature, error) { + blindedSignatures, recoverySigsDb, err := m.Signer.SignBlindMessages(request.Outputs) + if err != nil { + return nil, fmt.Errorf("m.Signer.SignBlindMessages(request.Outputs) %w", err) + } + mintRequestDB.State = cashu.ISSUED + mintRequestDB.Minted = true + afterMintingTx, err := m.MintDB.GetTx(ctx) + if err != nil { + return nil, fmt.Errorf(" m.MintDB.GetTx(ctx). %w", err) + } + defer func() { + rollbackErr := m.MintDB.Rollback(ctx, afterMintingTx) + if rollbackErr != nil { + if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + err = m.MintDB.ChangeMintRequestState(afterMintingTx, mintRequestDB.Quote, mintRequestDB.State, mintRequestDB.Minted) + if err != nil { + return nil, fmt.Errorf("m.MintDB.ChangeMintRequestState. %w", err) + } + + slog.Debug(fmt.Sprintf("Saving restore sigs for quote: id %v", mintRequestDB.Quote)) + err = m.MintDB.SaveRestoreSigs(afterMintingTx, recoverySigsDb) + if err != nil { + return nil, fmt.Errorf("m.MintDB.SaveRestoreSigs. %w", err) + } + err = m.MintDB.Commit(ctx, afterMintingTx) + if err != nil { + return nil, fmt.Errorf(" m.MintDB.Commit(ctx, tx). %w", err) + } + go m.Observer.SendMintEvent(mintRequestDB) + return blindedSignatures, nil +} + +func (m *Mint) validateMintStatusAndAuth(request cashu.PostMintBolt11Request, mintRequestDB cashu.MintRequestDB) error { + if mintRequestDB.Minted { + return cashu.ErrMintRequestAlreadyIssued + } + + if mintRequestDB.Pubkey.PublicKey != nil { + valid, err := request.VerifyPubkey(mintRequestDB.Pubkey.PublicKey) + if err != nil { + return fmt.Errorf("request.VerifyPubkey(mintRequestDB.Pubkey.PublicKey). %w", err) + } + + if !valid { + return fmt.Errorf("invalid pubkey signature. %w", err) + } + } + return nil +} diff --git a/internal/mint/proofs.go b/internal/mint/proofs.go index 0c990fb7..438ecd0b 100644 --- a/internal/mint/proofs.go +++ b/internal/mint/proofs.go @@ -12,13 +12,13 @@ import ( ) func CheckProofState(ctx context.Context, mint *Mint, Ys []cashu.WrappedPublicKey) ([]cashu.CheckState, error) { - var states []cashu.CheckState tx, err := mint.MintDB.GetTx(ctx) if err != nil { - return states, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) + return nil, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) } defer func() { - if err := mint.MintDB.Rollback(ctx, tx); err != nil { + err := mint.MintDB.Rollback(ctx, tx) + if err != nil { if !errors.Is(err, pgx.ErrTxClosed) { slog.Warn("rotate keyset sql transaction error", slog.Any("error", err)) } @@ -28,18 +28,18 @@ func CheckProofState(ctx context.Context, mint *Mint, Ys []cashu.WrappedPublicKe // set as unspent proofs, err := mint.MintDB.GetProofsFromSecretCurve(tx, Ys) if err != nil { - return states, fmt.Errorf("database.CheckListOfProofsBySecretCurve(pool, Ys). %w", err) + return nil, fmt.Errorf("database.CheckListOfProofsBySecretCurve(pool, Ys). %w", err) } err = mint.MintDB.Commit(ctx, tx) if err != nil { - return states, fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err) + return nil, fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err) } proofsForRemoval := make([]cashu.Proof, 0) - for _, state := range Ys { - + var states = make([]cashu.CheckState, len(Ys)) + for i, state := range Ys { pendingAndSpent := false checkState := cashu.CheckState{ @@ -56,7 +56,6 @@ func CheckProofState(ctx context.Context, mint *Mint, Ys []cashu.WrappedPublicKe checkState.Witness = &p.Witness } if compare && pendingAndSpent { - proofsForRemoval = append(proofsForRemoval, p) } return compare @@ -64,7 +63,7 @@ func CheckProofState(ctx context.Context, mint *Mint, Ys []cashu.WrappedPublicKe checkState.State = cashu.PROOF_SPENT } - states = append(states, checkState) + states[i] = checkState } return states, nil diff --git a/internal/mint/restore.go b/internal/mint/restore.go new file mode 100644 index 00000000..19298bc4 --- /dev/null +++ b/internal/mint/restore.go @@ -0,0 +1,40 @@ +package mint + +import ( + "context" + "fmt" + + "github.com/lescuer97/nutmix/api/cashu" +) + +func (m *Mint) Restore(ctx context.Context, request cashu.PostRestoreRequest) (cashu.PostRestoreResponse, error) { + blindingFactors := make([]cashu.WrappedPublicKey, len(request.Outputs)) + + for i, output := range request.Outputs { + blindingFactors[i] = output.B_ + } + + tx, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.PostRestoreResponse{}, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) + } + + blindRecoverySigs, err := m.MintDB.GetRestoreSigsFromBlindedMessages(tx, blindingFactors) + if err != nil { + return cashu.PostRestoreResponse{}, fmt.Errorf("m.MintDB.GetRestoreSigsFromBlindedMessages(tx, blindingFactors) %w", err) + } + err = m.MintDB.Commit(ctx, tx) + if err != nil { + return cashu.PostRestoreResponse{}, fmt.Errorf("m.MintDB.Commit(ctx, tx) %w", err) + } + + restoredBlindSigs := make([]cashu.BlindSignature, len(blindRecoverySigs)) + restoredBlindMessage := make([]cashu.BlindedMessage, len(blindRecoverySigs)) + + for i, sigRecover := range blindRecoverySigs { + restoredSig, restoredMessage := sigRecover.GetSigAndMessage() + restoredBlindSigs[i] = restoredSig + restoredBlindMessage[i] = restoredMessage + } + return cashu.PostRestoreResponse{Signatures: restoredBlindSigs, Promises: restoredBlindSigs, Outputs: restoredBlindMessage}, nil +} diff --git a/internal/mint/seeds.go b/internal/mint/seeds.go index 9b8dcb5d..711973f3 100644 --- a/internal/mint/seeds.go +++ b/internal/mint/seeds.go @@ -11,7 +11,6 @@ type SeedType struct { } func CheckForInactiveSeeds(seeds []cashu.Seed) ([]SeedType, error) { - seedTypes := make(map[cashu.Unit]SeedType) for _, seed := range seeds { @@ -21,14 +20,12 @@ func CheckForInactiveSeeds(seeds []cashu.Seed) ([]SeedType, error) { return nil, err } if seed.Version > seedTypes[unit].Version { - seedTypes[unit] = SeedType{ Version: seed.Version, Active: seed.Active, Unit: unit, } } - } inactiveSeeds := make([]SeedType, 0) diff --git a/internal/mint/swapping.go b/internal/mint/swapping.go new file mode 100644 index 00000000..8c77621c --- /dev/null +++ b/internal/mint/swapping.go @@ -0,0 +1,228 @@ +package mint + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/jackc/pgx/v5" + "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/utils" +) + +func (m *Mint) Swap(ctx context.Context, request cashu.PostSwapRequest) (cashu.PostSwapResponse, error) { + amountValidationErr := m.swapRequestValidateAmount(request) + if amountValidationErr != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.validateInputAndOutput(request.Inputs, request.Outputs). %w", amountValidationErr) + } + + // validate sig all + err := m.validateSwapRequest(request) + if err != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.validateInputAndOutput(request.Inputs, request.Outputs). %w", err) + } + + // check if proofs are spent and if outputs are spent + sizeCheckTx, err := m.MintDB.GetTx(ctx) + if err != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) + } + defer func() { + if err != nil { + rollbackErr := m.MintDB.Rollback(ctx, sizeCheckTx) + if rollbackErr != nil { + slog.Warn("rollback error", slog.Any("error", rollbackErr)) + } + } + }() + + proofs, err := m.checkProofSpent(sizeCheckTx, request.Inputs) + if err != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.checkProofSpent(sizeCheckTx, request.Inputs). %w", err) + } + + err = m.CheckOutputSpent(sizeCheckTx, request.Outputs) + if err != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + } + + proofs.SetProofsState(cashu.PROOF_PENDING) + err = m.MintDB.SaveProof(sizeCheckTx, proofs) + if err != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + } + + err = sizeCheckTx.Commit(ctx) + if err != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + } + + blindSignatures, err := m.signAndSetInputs(ctx, proofs, request) + if err != nil { + return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + } + + // mark as pending and sign + return cashu.PostSwapResponse{ + Signatures: blindSignatures, + }, nil +} + +func (m *Mint) signAndSetInputs(ctx context.Context, inputs cashu.Proofs, swapRequest cashu.PostSwapRequest) ([]cashu.BlindSignature, error) { + // sign the outputs + blindedSignatures, recoverySigsDb, err := m.Signer.SignBlindMessages(swapRequest.Outputs) + if err != nil { + return nil, fmt.Errorf("m.Signer.SignBlindMessages(outputs). %w", err) + } + + afterSigningTx, err := m.MintDB.GetTx(ctx) + if err != nil { + return nil, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + } + defer func() { + rollbackErr := m.MintDB.Rollback(ctx, afterSigningTx) + if rollbackErr != nil { + if !errors.Is(rollbackErr, pgx.ErrTxClosed) { + slog.Warn("could not swap state", slog.Any("error", rollbackErr)) + } + } + }() + err = m.MintDB.SetProofsState(afterSigningTx, inputs, cashu.PROOF_SPENT) + if err != nil { + return nil, fmt.Errorf("m.MintDB.SetProofsState(afterSigningTx, inputs, cashu.PROOF_SPENT). %w", err) + } + err = m.MintDB.SaveRestoreSigs(afterSigningTx, recoverySigsDb) + if err != nil { + return nil, fmt.Errorf("m.MintDB.SaveRestoreSigs(afterSigningTx, recoverySigsDb). %w", err) + } + err = m.MintDB.Commit(ctx, afterSigningTx) + if err != nil { + return nil, fmt.Errorf("m.MintDB.Commit(ctx, afterSigningTx). %w", err) + } + return blindedSignatures, nil +} + +func (m *Mint) validateSwapRequest(request cashu.PostSwapRequest) error { + // validate if the proofs are correctly signed + err := m.VerifyProofsBDHKE(request.Inputs) + if err != nil { + return fmt.Errorf("m.VerifyProofsBDHKE(proofs). %w", err) + } + + // Verify spending conditions - EXCLUSIVE paths following CDK pattern + hasSigAll, err := cashu.ProofsHaveSigAll(request.Inputs) + if err != nil { + return fmt.Errorf("cashu.ProofsHaveSigAll(inputs). %w", err) + } + + if hasSigAll { + // SIG_ALL path: verify all conditions match and signature is valid against combined message + err = request.ValidateSigflag() + if err != nil { + return fmt.Errorf("request.ValidateSigflag(). %w", err) + } + } else { + // Individual verification path: verify each proof's P2PK/HTLC spend conditions + err = cashu.VerifyProofsSpendConditions(request.Inputs) + if err != nil { + return fmt.Errorf("cashu.VerifyProofsSpendConditions(request.Inputs). %w", err) + } + } + return nil +} + +func (m *Mint) swapRequestValidateAmount(request cashu.PostSwapRequest) error { + if len(request.Inputs) == 0 || len(request.Outputs) == 0 { + return fmt.Errorf("inputs or outputs are empty") + } + proofsAmount := request.Inputs.Amount() + blindMessageAmount := request.Outputs.Amount() + + keysets, err := m.Signer.GetKeysets() + if err != nil { + return err + } + + // check for needed amount of fees + fee, err := cashu.Fees(request.Inputs, keysets.Keysets) + if err != nil { + return fmt.Errorf("cashu.Fees(request.Inputs, keysets.Keysets). %w", err) + } + + balance := (proofsAmount - (uint64(fee) + blindMessageAmount)) + if balance != 0 { + return fmt.Errorf("(proofs.Amount() - (uint64(fee) + AmountSignature)). %w", cashu.ErrUnbalanced) + } + + // get unit from proofs + proofUnit, err := checkProofsAreSameUnit(request.Inputs, keysets.Keysets) + if err != nil { + return fmt.Errorf("m.CheckProofsAreSameUnit(proofs, keysets.Keysets). %w", err) + } + + // check if outputs are + outputUnit, err := verifyOutputs(request.Outputs, keysets.Keysets) + if err != nil { + return fmt.Errorf("m.VerifyOutputs(outputs). %w", err) + } + + if proofUnit != outputUnit { + return fmt.Errorf("proofUnit != messageUnit. %w", cashu.ErrNotSameUnits) + } + + return nil +} + +// returns the proofs with the Y's and seen at. +func (m *Mint) checkProofSpent(tx pgx.Tx, proofs cashu.Proofs) (cashu.Proofs, error) { + // gets the list of Y's. You need to calculate + YsList, err := utils.GetAndCalculateProofsValues(&proofs) + if err != nil { + return nil, fmt.Errorf("utils.GetAndCalculateProofsValues(&proofs). %w", err) + } + + // check if we know any of the proofs + knownProofs, err := m.MintDB.GetProofsFromSecretCurve(tx, YsList) + if err != nil { + return nil, fmt.Errorf("m.MintDB.GetProofsFromSecretCurve(tx, SecretsList). %w", err) + } + + if len(knownProofs) != 0 { + for _, p := range knownProofs { + if p.State == cashu.PROOF_PENDING { + return nil, cashu.ErrProofPending + } + } + return nil, cashu.ErrProofSpent + } + + return proofs, nil +} +func (m *Mint) CheckOutputSpent(tx pgx.Tx, blindedMessages cashu.BlindedMessages) error { + outputsMap := make(map[string]bool) + blindingFactors := []cashu.WrappedPublicKey{} + + // Check if there is a repeated output, if not add it to the blindingFactors + for _, output := range blindedMessages { + outputKey := output.B_.String() + exists := outputsMap[outputKey] + if exists { + return cashu.ErrRepeatedOutput + } + outputsMap[outputKey] = true + + blindingFactors = append(blindingFactors, output.B_) + } + + blindRecoverySigs, err := m.MintDB.GetRestoreSigsFromBlindedMessages(tx, blindingFactors) + if err != nil { + return fmt.Errorf("m.GetRestorySigsFromBlindFactor(blindingFactors). %w", err) + } + + if len(blindRecoverySigs) != 0 { + return fmt.Errorf("blind message already has been signed: %w", cashu.ErrBlindMessageAlreadySigned) + } + + return nil +} diff --git a/internal/mint/transaction_persistence_test.go b/internal/mint/transaction_persistence_test.go new file mode 100644 index 00000000..7dd9b5a2 --- /dev/null +++ b/internal/mint/transaction_persistence_test.go @@ -0,0 +1,628 @@ +package mint + +import ( + "context" + crand "crypto/rand" + "encoding/hex" + "testing" + "time" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/lescuer97/nutmix/api/cashu" + "github.com/lescuer97/nutmix/internal/lightning" + "github.com/lescuer97/nutmix/internal/signer" + internalutils "github.com/lescuer97/nutmix/internal/utils" + nutcrypto "github.com/lescuer97/nutmix/pkg/crypto" + "github.com/lightningnetwork/lnd/zpay32" +) + +type quoteAmountBackend struct { + lightning.FakeWallet + feesResponse lightning.FeesResponse +} + +func (b quoteAmountBackend) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mpp bool, amount cashu.Amount) (lightning.FeesResponse, error) { + return b.feesResponse, nil +} + +func createMintTestBlindedMessagesWithSecrets(t *testing.T, amount uint64, activeKeys signer.GetKeysResponse) (cashu.BlindedMessages, []string, []*secp256k1.PrivateKey) { + t.Helper() + + splitAmounts := cashu.AmountSplit(amount) + blindedMessages := make(cashu.BlindedMessages, len(splitAmounts)) + secrets := make([]string, len(splitAmounts)) + blindingFactors := make([]*secp256k1.PrivateKey, len(splitAmounts)) + + for i, amt := range splitAmounts { + r, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatalf("secp256k1.GeneratePrivateKey(): %v", err) + } + blindingFactors[i] = r + + for { + secretBytes := make([]byte, 32) + _, err = crand.Read(secretBytes) + if err != nil { + t.Fatalf("rand.Read(secretBytes): %v", err) + } + + secret := hex.EncodeToString(secretBytes) + B_, _, blindErr := nutcrypto.BlindMessage(secret, r) + if blindErr == nil { + blindedMessages[i] = cashu.BlindedMessage{ + Amount: amt, + B_: cashu.WrappedPublicKey{PublicKey: B_}, + Id: activeKeys.Keysets[0].Id, + Witness: "", + } + secrets[i] = secret + break + } + } + } + + return blindedMessages, secrets, blindingFactors +} + +func createMintTestBlindedMessages(t *testing.T, amount uint64, activeKeys signer.GetKeysResponse) cashu.BlindedMessages { + blindedMessages, _, _ := createMintTestBlindedMessagesWithSecrets(t, amount, activeKeys) + return blindedMessages +} + +func createSpendableProofs(t *testing.T, mint *Mint, amount uint64, activeKeys signer.GetKeysResponse) cashu.Proofs { + t.Helper() + + blindedMessages, secrets, blindingFactors := createMintTestBlindedMessagesWithSecrets(t, amount, activeKeys) + blindSignatures, _, err := mint.Signer.SignBlindMessages(blindedMessages) + if err != nil { + t.Fatalf("mint.Signer.SignBlindMessages(blindedMessages): %v", err) + } + + proofs := make(cashu.Proofs, len(blindSignatures)) + for i, blindSignature := range blindSignatures { + pubkeyStr := activeKeys.Keysets[0].Keys[blindSignature.Amount] + pubkeyBytes, err := hex.DecodeString(pubkeyStr) + if err != nil { + t.Fatalf("hex.DecodeString(pubkeyStr): %v", err) + } + + mintPublicKey, err := secp256k1.ParsePubKey(pubkeyBytes) + if err != nil { + t.Fatalf("secp256k1.ParsePubKey(pubkeyBytes): %v", err) + } + + C := nutcrypto.UnblindSignature(blindSignature.C_.PublicKey, blindingFactors[i], mintPublicKey) + proofs[i] = cashu.Proof{ + Id: blindSignature.Id, + Amount: blindSignature.Amount, + C: cashu.WrappedPublicKey{PublicKey: C}, + Secret: secrets[i], + Y: cashu.WrappedPublicKey{PublicKey: nil}, + Quote: nil, + Witness: "", + State: cashu.PROOF_UNSPENT, + SeenAt: 0, + } + } + + return proofs +} + +func createMeltTestProofs(t *testing.T, amount uint64, activeKeys signer.GetKeysResponse) cashu.Proofs { + t.Helper() + + splitAmounts := cashu.AmountSplit(amount) + proofs := make(cashu.Proofs, len(splitAmounts)) + + for i, amt := range splitAmounts { + secretBytes := make([]byte, 32) + _, err := crand.Read(secretBytes) + if err != nil { + t.Fatalf("rand.Read(secretBytes): %v", err) + } + + cPriv, err := secp256k1.GeneratePrivateKey() + if err != nil { + t.Fatalf("secp256k1.GeneratePrivateKey(): %v", err) + } + + proof := cashu.Proof{ + Amount: amt, + C: cashu.WrappedPublicKey{PublicKey: cPriv.PubKey()}, + Id: activeKeys.Keysets[0].Id, + Secret: hex.EncodeToString(secretBytes), + Y: cashu.WrappedPublicKey{PublicKey: nil}, + Quote: nil, + Witness: "", + State: cashu.PROOF_UNSPENT, + SeenAt: 0, + } + + proof, err = proof.HashSecretToCurve() + if err != nil { + t.Fatalf("proof.HashSecretToCurve(): %v", err) + } + + proofs[i] = proof + } + + return proofs +} + +func TestSignAndSaveSigsPersistsIssuedState(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + ctx := context.Background() + + activeKeys, err := mint.Signer.GetActiveKeys() + if err != nil { + t.Fatalf("mint.Signer.GetActiveKeys(): %v", err) + } + + amount := uint64(2) + mintRequest := cashu.MintRequestDB{ + Amount: &amount, + Pubkey: cashu.WrappedPublicKey{PublicKey: nil}, + Description: nil, + Quote: "sign-save-quote", + Request: RegtestRequest, + Unit: cashu.Sat.String(), + State: cashu.PAID, + CheckingId: "check-sign-save", + Expiry: time.Now().Add(time.Minute).Unix(), + SeenAt: time.Now().Unix(), + Minted: false, + } + + tx, err := mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + err = mint.MintDB.SaveMintRequest(tx, mintRequest) + if err != nil { + t.Fatalf("mint.MintDB.SaveMintRequest(tx, mintRequest): %v", err) + } + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } + + outputs := createMintTestBlindedMessages(t, amount, activeKeys) + _, err = mint.signAndSaveSigs(ctx, cashu.PostMintBolt11Request{Signature: nil, Quote: mintRequest.Quote, Outputs: outputs}, mintRequest) + if err != nil { + t.Fatalf("mint.signAndSaveSigs(ctx, request, mintRequest): %v", err) + } + + tx, err = mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + savedMintRequest, err := mint.MintDB.GetMintRequestById(tx, mintRequest.Quote) + if err != nil { + t.Fatalf("mint.MintDB.GetMintRequestById(tx, mintRequest.Quote): %v", err) + } + + if savedMintRequest.State != cashu.ISSUED { + t.Fatalf("expected saved mint request to be ISSUED, got %v", savedMintRequest.State) + } + if !savedMintRequest.Minted { + t.Fatalf("expected saved mint request to be marked minted") + } + + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } +} + +func TestSettleIfInternalMeltPersistsMintRequestState(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + ctx := context.Background() + + amount := uint64(2) + mintRequest := cashu.MintRequestDB{ + Amount: &amount, + Pubkey: cashu.WrappedPublicKey{PublicKey: nil}, + Description: nil, + Quote: "internal-mint-quote", + Request: RegtestRequest, + Unit: cashu.Sat.String(), + State: cashu.UNPAID, + CheckingId: "internal-mint-check", + Expiry: time.Now().Add(time.Minute).Unix(), + SeenAt: time.Now().Unix(), + Minted: false, + } + meltQuote := cashu.MeltRequestDB{ + Amount: amount, + Quote: "internal-melt-quote", + Request: RegtestRequest, + Unit: cashu.Sat.String(), + Expiry: time.Now().Add(time.Minute).Unix(), + FeeReserve: 0, + State: cashu.PENDING, + PaymentPreimage: "", + SeenAt: time.Now().Unix(), + Mpp: false, + FeePaid: 0, + Melted: false, + CheckingId: "internal-melt-check", + } + + tx, err := mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + err = mint.MintDB.SaveMintRequest(tx, mintRequest) + if err != nil { + t.Fatalf("mint.MintDB.SaveMintRequest(tx, mintRequest): %v", err) + } + err = mint.MintDB.SaveMeltRequest(tx, meltQuote) + if err != nil { + t.Fatalf("mint.MintDB.SaveMeltRequest(tx, meltQuote): %v", err) + } + + settledQuote, err := mint.settleIfInternalMelt(tx, meltQuote) + if err != nil { + t.Fatalf("mint.settleIfInternalMelt(tx, meltQuote): %v", err) + } + if settledQuote.State != cashu.PAID { + t.Fatalf("expected settled quote state to be PAID, got %v", settledQuote.State) + } + if !settledQuote.Melted { + t.Fatalf("expected settled quote to be marked melted") + } + + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } + + tx, err = mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + savedMintRequest, err := mint.MintDB.GetMintRequestById(tx, mintRequest.Quote) + if err != nil { + t.Fatalf("mint.MintDB.GetMintRequestById(tx, mintRequest.Quote): %v", err) + } + if savedMintRequest.State != cashu.PAID { + t.Fatalf("expected mint request state to be PAID, got %v", savedMintRequest.State) + } + + savedMeltQuote, err := mint.MintDB.GetMeltRequestById(tx, meltQuote.Quote) + if err != nil { + t.Fatalf("mint.MintDB.GetMeltRequestById(tx, meltQuote.Quote): %v", err) + } + if savedMeltQuote.State != cashu.PAID { + t.Fatalf("expected melt quote state to be PAID, got %v", savedMeltQuote.State) + } + if !savedMeltQuote.Melted { + t.Fatalf("expected melt quote to be marked melted") + } + + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } +} + +func TestValidateMeltStatusAndSpentPersistsProofQuoteReference(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + ctx := context.Background() + + activeKeys, err := mint.Signer.GetActiveKeys() + if err != nil { + t.Fatalf("mint.Signer.GetActiveKeys(): %v", err) + } + + meltQuote := cashu.MeltRequestDB{ + Amount: 2, + Quote: "pending-proof-quote", + Request: RegtestRequest, + Unit: cashu.Sat.String(), + Expiry: time.Now().Add(time.Minute).Unix(), + FeeReserve: 1, + State: cashu.UNPAID, + PaymentPreimage: "", + SeenAt: time.Now().Unix(), + Mpp: false, + CheckingId: "pending-proof-check", + FeePaid: 0, + Melted: false, + } + + tx, err := mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + err = mint.MintDB.SaveMeltRequest(tx, meltQuote) + if err != nil { + t.Fatalf("mint.MintDB.SaveMeltRequest(tx, meltQuote): %v", err) + } + + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } + + request := cashu.PostMeltBolt11Request{ + Quote: meltQuote.Quote, + Inputs: createMeltTestProofs(t, 4, activeKeys), + Outputs: createMintTestBlindedMessages(t, 1, activeKeys), + } + + savedQuote, err := mint.validateMeltStatusAndSpent(ctx, request) + if err != nil { + t.Fatalf("mint.validateMeltStatusAndSpent(ctx, request): %v", err) + } + + if savedQuote.State != cashu.PENDING { + t.Fatalf("expected saved quote state to be PENDING, got %v", savedQuote.State) + } + + tx, err = mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + savedProofs, err := mint.MintDB.GetProofsFromQuote(tx, meltQuote.Quote) + if err != nil { + t.Fatalf("mint.MintDB.GetProofsFromQuote(tx, meltQuote.Quote): %v", err) + } + + if len(savedProofs) != len(request.Inputs) { + t.Fatalf("expected %d saved proofs, got %d", len(request.Inputs), len(savedProofs)) + } + + for _, proof := range savedProofs { + if proof.Quote == nil || *proof.Quote != meltQuote.Quote { + t.Fatalf("expected proof quote reference %q, got %+v", meltQuote.Quote, proof.Quote) + } + if proof.State != cashu.PROOF_PENDING { + t.Fatalf("expected proof state PENDING, got %v", proof.State) + } + } + + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } +} + +func TestMeltQuoteUsesBackendAmountToSend(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + mint.LightningBackend = quoteAmountBackend{ + FakeWallet: lightning.FakeWallet{ + Network: *mint.LightningBackend.GetNetwork(), + UnpurposeErrors: []lightning.FakeWalletError{}, + InvoiceFee: 0, + }, + feesResponse: lightning.FeesResponse{ + CheckingId: "backend-checking-id", + Fees: cashu.NewAmount(cashu.Sat, 0), + AmountToSend: cashu.NewAmount(cashu.Sat, 2), + }, + } + + quote, err := mint.MeltQuote(context.Background(), cashu.PostMeltQuoteBolt11Request{ + Options: cashu.PostMeltQuoteBolt11Options{Mpp: nil}, + Request: RegtestRequest, + Unit: cashu.Sat.String(), + }, Bolt11) + if err != nil { + t.Fatalf("mint.MeltQuote(context.Background(), request, Bolt11): %v", err) + } + + if quote.Amount != 2 { + t.Fatalf("expected melt quote amount to use backend amount 2, got %d", quote.Amount) + } + if quote.CheckingId != "backend-checking-id" { + t.Fatalf("expected checking id to use backend response, got %q", quote.CheckingId) + } +} + +func TestSignAndSaveSigsSendsMintEvent(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + ctx := context.Background() + + activeKeys, err := mint.Signer.GetActiveKeys() + if err != nil { + t.Fatalf("mint.Signer.GetActiveKeys(): %v", err) + } + + amount := uint64(2) + mintRequest := cashu.MintRequestDB{ + Amount: &amount, + Pubkey: cashu.WrappedPublicKey{PublicKey: nil}, + Description: nil, + Quote: "mint-event-quote", + Request: RegtestRequest, + Unit: cashu.Sat.String(), + State: cashu.PAID, + CheckingId: "mint-event-check", + Expiry: time.Now().Add(time.Minute).Unix(), + SeenAt: time.Now().Unix(), + Minted: false, + } + + tx, err := mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + err = mint.MintDB.SaveMintRequest(tx, mintRequest) + if err != nil { + t.Fatalf("mint.MintDB.SaveMintRequest(tx, mintRequest): %v", err) + } + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } + + mintChan := make(chan cashu.MintRequestDB, 1) + mint.Observer.AddMintWatch(mintRequest.Quote, MintQuoteChannel{SubId: "mint-event", Channel: mintChan}) + + outputs := createMintTestBlindedMessages(t, amount, activeKeys) + _, err = mint.signAndSaveSigs(ctx, cashu.PostMintBolt11Request{Signature: nil, Quote: mintRequest.Quote, Outputs: outputs}, mintRequest) + if err != nil { + t.Fatalf("mint.signAndSaveSigs(ctx, request, mintRequest): %v", err) + } + + select { + case observedMint := <-mintChan: + if observedMint.Quote != mintRequest.Quote { + t.Fatalf("expected mint event quote %q, got %q", mintRequest.Quote, observedMint.Quote) + } + if observedMint.State != cashu.ISSUED { + t.Fatalf("expected mint event state ISSUED, got %v", observedMint.State) + } + if !observedMint.Minted { + t.Fatalf("expected mint event to be marked minted") + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for mint event") + } +} + +func TestBolt11MeltSendsSuccessEvents(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + ctx := context.Background() + + activeKeys, err := mint.Signer.GetActiveKeys() + if err != nil { + t.Fatalf("mint.Signer.GetActiveKeys(): %v", err) + } + + proofs := createSpendableProofs(t, mint, 4, activeKeys) + proofsForWatch := append(cashu.Proofs(nil), proofs...) + _, err = internalutils.GetAndCalculateProofsValues(&proofsForWatch) + if err != nil { + t.Fatalf("internalutils.GetAndCalculateProofsValues(&proofsForWatch): %v", err) + } + + mintAmount := uint64(2) + mintRequest := cashu.MintRequestDB{ + Amount: &mintAmount, + Pubkey: cashu.WrappedPublicKey{PublicKey: nil}, + Description: nil, + Quote: "success-event-mint-quote", + Request: RegtestRequest, + Unit: cashu.Sat.String(), + State: cashu.UNPAID, + CheckingId: "success-event-mint-check", + Expiry: time.Now().Add(time.Minute).Unix(), + SeenAt: time.Now().Unix(), + Minted: false, + } + meltQuote := cashu.MeltRequestDB{ + Amount: mintAmount, + Quote: "success-event-melt-quote", + Request: RegtestRequest, + Unit: cashu.Sat.String(), + Expiry: time.Now().Add(time.Minute).Unix(), + FeeReserve: 0, + State: cashu.UNPAID, + PaymentPreimage: "", + SeenAt: time.Now().Unix(), + Mpp: false, + FeePaid: 0, + Melted: false, + CheckingId: "success-event-melt-check", + } + + tx, err := mint.MintDB.GetTx(ctx) + if err != nil { + t.Fatalf("mint.MintDB.GetTx(ctx): %v", err) + } + defer func() { + _ = mint.MintDB.Rollback(ctx, tx) + }() + + err = mint.MintDB.SaveMintRequest(tx, mintRequest) + if err != nil { + t.Fatalf("mint.MintDB.SaveMintRequest(tx, mintRequest): %v", err) + } + err = mint.MintDB.SaveMeltRequest(tx, meltQuote) + if err != nil { + t.Fatalf("mint.MintDB.SaveMeltRequest(tx, meltQuote): %v", err) + } + err = mint.MintDB.Commit(ctx, tx) + if err != nil { + t.Fatalf("mint.MintDB.Commit(ctx, tx): %v", err) + } + + proofChan := make(chan cashu.Proof, len(proofsForWatch)) + meltChan := make(chan cashu.MeltRequestDB, 1) + for _, proof := range proofsForWatch { + mint.Observer.AddProofWatch(proof.Y.ToHex(), ProofWatchChannel{SubId: "melt-proof-event", Channel: proofChan}) + } + mint.Observer.AddMeltWatch(meltQuote.Quote, MeltQuoteChannel{SubId: "melt-quote-event", Channel: meltChan}) + + quote, response, err := mint.bolt11Melt(ctx, cashu.PostMeltBolt11Request{Quote: meltQuote.Quote, Inputs: proofs, Outputs: nil}) + if err != nil { + t.Fatalf("mint.bolt11Melt(ctx, request): %v", err) + } + + if quote.State != cashu.PAID { + t.Fatalf("expected melt quote state PAID, got %v", quote.State) + } + if response.State != cashu.PAID { + t.Fatalf("expected melt response state PAID, got %v", response.State) + } + + select { + case observedMelt := <-meltChan: + if observedMelt.Quote != meltQuote.Quote { + t.Fatalf("expected melt event quote %q, got %q", meltQuote.Quote, observedMelt.Quote) + } + if observedMelt.State != cashu.PAID { + t.Fatalf("expected melt event state PAID, got %v", observedMelt.State) + } + if !observedMelt.Melted { + t.Fatalf("expected melt event to be marked melted") + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for melt event") + } + + for i := 0; i < len(proofsForWatch); i++ { + select { + case observedProof := <-proofChan: + if observedProof.State != cashu.PROOF_SPENT { + t.Fatalf("expected proof event state SPENT, got %v", observedProof.State) + } + if observedProof.Y.PublicKey == nil { + t.Fatal("expected proof event Y to be populated") + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for proof event") + } + } +} diff --git a/internal/mint/utils.go b/internal/mint/utils.go index 06d32045..36f74b88 100644 --- a/internal/mint/utils.go +++ b/internal/mint/utils.go @@ -13,7 +13,6 @@ import ( func (m *Mint) GetChangeOutput(messages []cashu.BlindedMessage, overPaidFees uint64, unit string) ([]cashu.RecoverSigDB, error) { if overPaidFees > 0 && len(messages) > 0 { - change := utils.GetMessagesForChange(overPaidFees, messages) _, recoverySigsDb, err := m.Signer.SignBlindMessages(change) @@ -23,7 +22,6 @@ func (m *Mint) GetChangeOutput(messages []cashu.BlindedMessage, overPaidFees uin } return recoverySigsDb, nil - } return []cashu.RecoverSigDB{}, nil } @@ -42,7 +40,7 @@ func (m *Mint) VerifyUnitSupport(unitStr string) error { return nil } -func (m *Mint) checkMessagesAreSameUnit(messages []cashu.BlindedMessage, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { +func checkMessagesAreSameUnit(messages []cashu.BlindedMessage, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { units := make(map[string]bool) seenKeys := make(map[string]cashu.BasicKeysetResponse) @@ -51,7 +49,6 @@ func (m *Mint) checkMessagesAreSameUnit(messages []cashu.BlindedMessage, keys [] seenKeys[v.Id] = v } for _, proof := range messages { - val, exists := seenKeys[proof.Id] if exists { @@ -77,87 +74,20 @@ func (m *Mint) checkMessagesAreSameUnit(messages []cashu.BlindedMessage, keys [] } return returnedUnit, nil - } -func (m *Mint) VerifyOutputs(tx pgx.Tx, outputs []cashu.BlindedMessage, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { +func verifyOutputs(outputs []cashu.BlindedMessage, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { // check output have the correct unit - unit, err := m.checkMessagesAreSameUnit(outputs, keys) + unit, err := checkMessagesAreSameUnit(outputs, keys) if err != nil { return unit, fmt.Errorf("m.checkMessagesAreSameUnit(outputs, keysets.Keysets). %w", err) } - outputsMap := make(map[string]bool) - blindingFactors := []cashu.WrappedPublicKey{} - - // Check if there is a repeated output, if not add it to the blindingFactors - for _, output := range outputs { - outputKey := output.B_.String() - exists := outputsMap[outputKey] - if exists { - return unit, cashu.ErrRepeatedOutput - } - outputsMap[outputKey] = true - - blindingFactors = append(blindingFactors, output.B_) - } - - blindRecoverySigs, err := m.MintDB.GetRestoreSigsFromBlindedMessages(tx, blindingFactors) - if err != nil { - return unit, fmt.Errorf("m.GetRestorySigsFromBlindFactor(blindingFactors). %w", err) - } - - if len(blindRecoverySigs) != 0 { - return unit, fmt.Errorf("blind message already has been signed: %w", cashu.ErrBlindMessageAlreadySigned) - } return unit, nil } -func (m *Mint) VerifyInputsAndOutputs(tx pgx.Tx, proofs cashu.Proofs, outputs []cashu.BlindedMessage) error { - keysets, err := m.Signer.GetKeysets() - if err != nil { - return fmt.Errorf("m.Signer.GetKeys(). %w", err) - } - - // get unit from proofs - proofUnit, err := m.CheckProofsAreSameUnit(proofs, keysets.Keysets) - if err != nil { - return fmt.Errorf("m.CheckProofsAreSameUnit(proofs, keysets.Keysets). %w", err) - } - - outputUnit, err := m.VerifyOutputs(tx, outputs, keysets.Keysets) - if err != nil { - return fmt.Errorf("m.VerifyOutputs(outputs). %w", err) - } - - if proofUnit != outputUnit { - return fmt.Errorf("proofUnit != messageUnit. %w", cashu.ErrNotSameUnits) - } - - // check for needed amount of fees - fee, err := cashu.Fees(proofs, keysets.Keysets) - if err != nil { - return fmt.Errorf("cashu.Fees(proofs, keysets.Keysets). %w", err) - } - - var AmountSignature uint64 - // Check out amount signature - for _, output := range outputs { - AmountSignature += output.Amount - } - - balance := (proofs.Amount() - (uint64(fee) + AmountSignature)) - if balance != 0 { - return fmt.Errorf("(proofs.Amount() - (uint64(fee) + AmountSignature)). %w", cashu.ErrUnbalanced) - } - - // Only verify BDHKE here since spend conditions are verified separately before calling this function - err = m.VerifyProofsBDHKE(proofs) - if err != nil { - return fmt.Errorf("m.VerifyProofsBDHKE(proofs). %w", err) - } - - return nil +func (m *Mint) VerifyOutputs(outputs []cashu.BlindedMessage, keys []cashu.BasicKeysetResponse) (cashu.Unit, error) { + return verifyOutputs(outputs, keys) } // VerifyProofsBDHKE verifies the BDHKE cryptographic signatures of the proofs. @@ -170,14 +100,14 @@ func (m *Mint) VerifyProofsBDHKE(proofs cashu.Proofs) error { return nil } -func (m *Mint) IsInternalTransaction(request string) (bool, error) { - ctx := context.Background() - tx, err := m.MintDB.GetTx(context.Background()) +func (m *Mint) IsInternalTransaction(ctx context.Context, request string) (bool, error) { + tx, err := m.MintDB.GetTx(ctx) if err != nil { - return false, fmt.Errorf("m.MintDB.GetTx(context.Background()). %w", err) + return false, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) } defer func() { - if rollbackErr := m.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := m.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } diff --git a/internal/mint/utils_test.go b/internal/mint/utils_test.go index c85cf369..a3256338 100644 --- a/internal/mint/utils_test.go +++ b/internal/mint/utils_test.go @@ -45,7 +45,7 @@ func TestIsInternalTransactionSuccess(t *testing.T) { if err != nil { t.Fatalf("mint.MintDB.Commit(ctx, tx): %+v ", err) } - isInternal, err := mint.IsInternalTransaction(RegtestRequest) + isInternal, err := mint.IsInternalTransaction(ctx, RegtestRequest) if err != nil { t.Fatalf("mint.IsInternalTransaction(RegtestRequest): %+v ", err) } @@ -53,7 +53,6 @@ func TestIsInternalTransactionSuccess(t *testing.T) { if !isInternal { t.Error("should be internal transaction") } - } func TestIsInternalTransactionFail(t *testing.T) { mint := SetupMintWithLightningMockPostgres(t) @@ -88,7 +87,7 @@ func TestIsInternalTransactionFail(t *testing.T) { if err != nil { t.Fatalf("mint.MintDB.Commit(ctx, tx): %+v ", err) } - isInternal, err := mint.IsInternalTransaction(RegtestRequest) + isInternal, err := mint.IsInternalTransaction(ctx, RegtestRequest) if err != nil { t.Fatalf("mint.IsInternalTransaction(RegtestRequest): %+v ", err) } @@ -96,7 +95,6 @@ func TestIsInternalTransactionFail(t *testing.T) { if isInternal { t.Error("should be external transaction") } - } func TestVerifyUnitOfProofFail(t *testing.T) { @@ -210,7 +208,6 @@ func TestVerifyUnitOfProofPass(t *testing.T) { if unit != cashu.Sat { t.Errorf("Unit should be Sat. %v", err) } - } func TestVerifyOutputsFailRepeatedOutput(t *testing.T) { @@ -221,10 +218,6 @@ func TestVerifyOutputsFailRepeatedOutput(t *testing.T) { t.Fatalf("mint.Signer.RotateKeyset(cashu.EUR, 0): %+v ", err) } - keysets, err := mint.Signer.GetKeysets() - if err != nil { - t.Fatalf("mint.Signer.GetKeys(): %+v ", err) - } b_bytes1, err := hex.DecodeString("02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d264bdc074209b107ba2") if err != nil { t.Fatalf("Error decoding b_bytes1: %+v", err) @@ -247,20 +240,15 @@ func TestVerifyOutputsFailRepeatedOutput(t *testing.T) { {B_: cashu.WrappedPublicKey{PublicKey: B_2}, Id: "0143cd3bb4a53bc6aeca481bb5ee707ea702939c83d9a86541be106c0e3dfcfe52", Witness: "", Amount: 0}, } - tx, err := mint.MintDB.GetTx(context.Background()) + tx, err := mint.MintDB.GetTx(t.Context()) if err != nil { - t.Fatalf("could not get transaction. %v", err) - + t.Fatalf("could not get tx: %+v", err) } - _, err = mint.VerifyOutputs(tx, outputs, keysets.Keysets) + err = mint.CheckOutputSpent(tx, outputs) if err == nil { t.Errorf("should have failed because of there are repeated outputs: %+v ", err) } if !errors.Is(err, cashu.ErrRepeatedOutput) { t.Errorf("Error there should be a repeated output. %v", err) } - err = tx.Commit(context.Background()) - if err != nil { - t.Fatalf("Could not commit tx: %+v ", err) - } } diff --git a/internal/routes/admin/auth.go b/internal/routes/admin/auth.go index b5b6af9e..fd88df06 100644 --- a/internal/routes/admin/auth.go +++ b/internal/routes/admin/auth.go @@ -121,15 +121,16 @@ func LoginPost(mint *mint.Mint, loginKey *secp256k1.PrivateKey, adminNostrPubkey defer func() { if p := recover(); p != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } } - } else if err != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } @@ -205,12 +206,10 @@ func LoginPost(mint *mint.Mint, loginKey *secp256k1.PrivateKey, adminNostrPubkey } func makeJWTToken(secret []byte) (string, error) { - token := jwt.New(jwt.SigningMethodHS256) string, err := token.SignedString(secret) if err != nil { return "", fmt.Errorf("token.SignedString(secret) %v", err) - } return string, nil } diff --git a/internal/routes/admin/crons.go b/internal/routes/admin/crons.go index 016ff906..e97c63fd 100644 --- a/internal/routes/admin/crons.go +++ b/internal/routes/admin/crons.go @@ -31,15 +31,16 @@ func CheckStatusOfLiquiditySwaps(mint *m.Mint, newLiquidity chan string) { defer func() { if p := recover(); p != nil { slog.Warn("Rolling back because of failure", slog.Any("error", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } } - } else if err != nil { slog.Warn("Rolling back because of failure", slog.Any("error", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } @@ -63,7 +64,6 @@ func CheckStatusOfLiquiditySwaps(mint *m.Mint, newLiquidity chan string) { for { func() { - // check if there are new liquidity swaps to check select { case swapId := <-newLiquidity: @@ -87,15 +87,16 @@ func CheckStatusOfLiquiditySwaps(mint *m.Mint, newLiquidity chan string) { defer func() { if p := recover(); p != nil { slog.Warn("Rolling back because of failure", slog.Any("error", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, swapTx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, swapTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } } - } else if err != nil { slog.Warn("Rolling back because of failure", slog.Any("error", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, swapTx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, swapTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } @@ -171,7 +172,6 @@ func CheckStatusOfLiquiditySwaps(mint *m.Mint, newLiquidity chan string) { case lightning.FAILED: swap.State = utils.LightningPaymentFail } - } afterCheckTx, err := mint.MintDB.GetTx(ctx) @@ -185,7 +185,8 @@ func CheckStatusOfLiquiditySwaps(mint *m.Mint, newLiquidity chan string) { defer func() { if p := recover(); p != nil { slog.Warn("Rolling back because of failure", slog.Any("error", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, afterCheckTx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, afterCheckTx) + if rollbackErr != nil { if !errors.Is(rollbackErr, pgx.ErrTxClosed) { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } @@ -215,11 +216,8 @@ func CheckStatusOfLiquiditySwaps(mint *m.Mint, newLiquidity chan string) { } }() } - }() slog.Debug("Sleeping for 2 seconds", slog.String("swaps", fmt.Sprintf("%v", swaps))) time.Sleep(2 * time.Second) - } - } diff --git a/internal/routes/admin/keysets.go b/internal/routes/admin/keysets.go index edf0c68f..9d2d5086 100644 --- a/internal/routes/admin/keysets.go +++ b/internal/routes/admin/keysets.go @@ -18,7 +18,6 @@ var ErrUnitNotCorrect = errors.New("unit not correct") var ErrNoExpiryTime = errors.New("no expiry time provided") func KeysetsPage(mint *m.Mint) gin.HandlerFunc { - return func(c *gin.Context) { ctx := c.Request.Context() availableUnits := []cashu.Unit{cashu.Sat, cashu.Msat, cashu.USD, cashu.EUR} @@ -35,7 +34,6 @@ func KeysetsPage(mint *m.Mint) gin.HandlerFunc { // c.HTML(400,"", nil) return } - } } func KeysetsLayoutPage(adminHandler *adminHandler) gin.HandlerFunc { @@ -56,9 +54,9 @@ func KeysetsLayoutPage(adminHandler *adminHandler) gin.HandlerFunc { } type RotateRequest struct { - Fee uint - Unit cashu.Unit - ExpireLimitHours uint + Fee uint `json:"fee,omitempty"` + Unit cashu.Unit `json:"unit,omitempty"` + ExpireLimitHours uint `json:"expire_limit,omitempty"` } func RotateSatsSeed(adminHandler *adminHandler) gin.HandlerFunc { @@ -142,7 +140,6 @@ func RotateSatsSeed(adminHandler *adminHandler) gin.HandlerFunc { if c.ContentType() == gin.MIMEJSON { c.JSON(200, nil) } else { - c.Header("HX-Trigger", "recharge-keyset") err := RenderSuccess(c, "Key successfully rotated") if err != nil { diff --git a/internal/routes/admin/lightning.go b/internal/routes/admin/lightning.go index 3a0e9a8d..289d1c7c 100644 --- a/internal/routes/admin/lightning.go +++ b/internal/routes/admin/lightning.go @@ -10,7 +10,6 @@ import ( func LightningDataFormFields(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { - backend := c.Query(m.MINT_LIGHTNING_BACKEND_ENV) ctx := c.Request.Context() diff --git a/internal/routes/admin/liquidity-manager.go b/internal/routes/admin/liquidity-manager.go index 31d7a6a2..3f7a3860 100644 --- a/internal/routes/admin/liquidity-manager.go +++ b/internal/routes/admin/liquidity-manager.go @@ -90,7 +90,6 @@ func SwapOutForm(mint *m.Mint) gin.HandlerFunc { ctx := c.Request.Context() milillisatBalance, err := mint.LightningBackend.WalletBalance() if err != nil { - slog.Warn( "mint.LightningComs.WalletBalance()", slog.String(utils.LogExtraInfo, err.Error())) @@ -140,14 +139,16 @@ func SwapOutRequest(mint *m.Mint) gin.HandlerFunc { if err != nil { // If the fees are acceptable, continue to create the Receive Payment slog.Warn("zpay32.Decode(invoice)", slog.Any("error", err)) - if err := RenderError(c, "Invalid Lightning Invoice"); err != nil { + err := RenderError(c, "Invalid Lightning Invoice") + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return } if decodedInvoice.MilliSat == nil { - if err := RenderError(c, "Invoice must have an amount"); err != nil { + err := RenderError(c, "Invoice must have an amount") + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return @@ -157,7 +158,8 @@ func SwapOutRequest(mint *m.Mint) gin.HandlerFunc { currentBalance, err := mint.LightningBackend.WalletBalance() if err != nil { slog.Warn("Could not fetch wallet balance", slog.Any("error", err)) - if err := RenderError(c, "Could not check wallet balance"); err != nil { + err := RenderError(c, "Could not check wallet balance") + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return @@ -182,7 +184,8 @@ func SwapOutRequest(mint *m.Mint) gin.HandlerFunc { _ = c.Error(fmt.Errorf("invoiceAmountSat.To(cashu.Sat). %w", convertErr)) return } - if err := RenderError(c, fmt.Sprintf("Insufficient funds: Have %d sats, need %d sats", currentBalance.Amount, invoiceAmountSat.Amount)); err != nil { + err := RenderError(c, fmt.Sprintf("Insufficient funds: Have %d sats, need %d sats", currentBalance.Amount, invoiceAmountSat.Amount)) + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return @@ -192,7 +195,8 @@ func SwapOutRequest(mint *m.Mint) gin.HandlerFunc { feesResponse, err := mint.LightningBackend.QueryFees(invoice, decodedInvoice, false, cashu.NewAmount(cashu.Sat, uint64(amount))) if err != nil { slog.Info("mint.LightningComs.PayInvoice", slog.Any("error", err)) - if err := RenderError(c, "Could not calculate fees or route not found"); err != nil { + err := RenderError(c, "Could not calculate fees or route not found") + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return @@ -223,13 +227,14 @@ func SwapOutRequest(mint *m.Mint) gin.HandlerFunc { defer func() { if p := recover(); p != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } - } else if err != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } } @@ -268,14 +273,16 @@ func SwapInRequest(mint *m.Mint, newLiquidity chan string) gin.HandlerFunc { amount, err := strconv.ParseUint(amountStr, 10, 64) if err != nil { - if err := RenderError(c, "Invalid amount"); err != nil { + err := RenderError(c, "Invalid amount") + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return } if amount <= 0 { - if err := RenderError(c, "Amount must be greater than 0"); err != nil { + err := RenderError(c, "Amount must be greater than 0") + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return @@ -283,11 +290,11 @@ func SwapInRequest(mint *m.Mint, newLiquidity chan string) gin.HandlerFunc { uuid := uuid.New().String() - //nolint:exhaustruct - resp, err := mint.LightningBackend.RequestInvoice(cashu.MintRequestDB{Quote: uuid}, cashu.Amount{Amount: amount, Unit: cashu.Sat}) + resp, err := mint.LightningBackend.RequestInvoice(cashu.Amount{Amount: amount, Unit: cashu.Sat}, nil) if err != nil { slog.Warn("mint.LightningBackend.RequestInvoice", slog.Any("error", err)) - if err := RenderError(c, "Could not generate invoice"); err != nil { + err := RenderError(c, "Could not generate invoice") + if err != nil { slog.Warn("failed to render error", slog.Any("error", err)) } return @@ -324,13 +331,14 @@ func SwapInRequest(mint *m.Mint, newLiquidity chan string) gin.HandlerFunc { defer func() { if p := recover(); p != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } - } else if err != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } } @@ -390,13 +398,14 @@ func SwapStateCheck(mint *m.Mint) gin.HandlerFunc { defer func() { if p := recover(); p != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } - } else if err != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } } @@ -446,13 +455,14 @@ func ConfirmSwapOutTransaction(mint *m.Mint, newLiquidity chan string) gin.Handl defer func() { if p := recover(); p != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } - } else if err != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("Failed to rollback transaction", slog.Any("error", rollbackErr)) } } @@ -499,7 +509,6 @@ func ConfirmSwapOutTransaction(mint *m.Mint, newLiquidity chan string) gin.Handl // Hardened error handling if err != nil || payment.PaymentState == lightning.FAILED || payment.PaymentState == lightning.UNKNOWN || payment.PaymentState == lightning.PENDING { - // if exception of lightning payment says fail do a payment status recheck. status, _, _, err := mint.LightningBackend.CheckPayed(swapRequest.LightningInvoice, decodedInvoice, swapRequest.CheckingId) @@ -515,7 +524,8 @@ func ConfirmSwapOutTransaction(mint *m.Mint, newLiquidity chan string) gin.Handl return } defer func() { - if err := lnStatusTx.Rollback(ctx); err != nil { + err := lnStatusTx.Rollback(ctx) + if err != nil { slog.Warn("rollback error", slog.Any("error", err)) } }() @@ -612,7 +622,6 @@ func LiquiditySummaryComponent(handler *adminHandler) gin.HandlerFunc { if err != nil { _ = c.Error(fmt.Errorf("component.Render(ctx, c.Writer). %w", err)) return - } } } diff --git a/internal/routes/admin/main.go b/internal/routes/admin/main.go index a9b36e28..a2b153a1 100644 --- a/internal/routes/admin/main.go +++ b/internal/routes/admin/main.go @@ -59,7 +59,6 @@ func ErrorHtmlMessageMiddleware() gin.HandlerFunc { return } } - } } @@ -239,7 +238,6 @@ func AdminRoutes(ctx context.Context, r *gin.Engine, mint *m.Mint) { // nolint: contextcheck go CheckStatusOfLiquiditySwaps(mint, newLiquidity) } - } func liquidityManagerMiddleware(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { diff --git a/internal/routes/admin/tabs_test.go b/internal/routes/admin/tabs_test.go index 11e9480c..04e75c3d 100644 --- a/internal/routes/admin/tabs_test.go +++ b/internal/routes/admin/tabs_test.go @@ -40,7 +40,6 @@ func TestCheckIntegerFromStringFailureBool(t *testing.T) { if err == nil { t.Error("Check limit should have failed. Because it should not allow float") } - } func mustNpub(t *testing.T) string { diff --git a/internal/routes/auth.go b/internal/routes/auth.go index 20b02539..194fe44b 100644 --- a/internal/routes/auth.go +++ b/internal/routes/auth.go @@ -1,7 +1,6 @@ package routes import ( - "context" "fmt" "log/slog" @@ -13,7 +12,6 @@ import ( func AuthActivatedMiddleware(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { - if !mint.Config.MINT_REQUIRE_AUTH { slog.Warn(fmt.Errorf("tried using route that does not exist because auth not being active").Error()) c.JSON(404, "route does not exists") @@ -74,15 +72,27 @@ func v1AuthRoutes(r *gin.Engine, mint *m.Mint) { return } - ctx := context.Background() - tx, err := mint.MintDB.GetTx(ctx) + amountBlindMessages := uint64(0) + + for _, blindMessage := range mintRequest.Outputs { + amountBlindMessages += blindMessage.Amount + // check all blind messages have the same unit + } + + if amountBlindMessages > mint.Config.MINT_AUTH_MAX_BLIND_TOKENS { + slog.Warn("Trying to mint auth tokens over the limit") + c.JSON(400, cashu.ErrorCodeToResponse(cashu.MAXIMUM_BAT_MINT_LIMIT_EXCEEDED, nil)) + return + } + tx, err := mint.MintDB.GetTx(c.Request.Context()) if err != nil { _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) return } defer func() { if err != nil { - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(c.Request.Context(), tx) + if rollbackErr != nil { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } } @@ -95,7 +105,7 @@ func v1AuthRoutes(r *gin.Engine, mint *m.Mint) { c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - unit, err := mint.VerifyOutputs(tx, mintRequest.Outputs, keysets.Keysets) + unit, err := mint.VerifyOutputs(mintRequest.Outputs, keysets.Keysets) if err != nil { slog.Warn("mint.VerifyOutputs(mintRequest.Outputs)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) @@ -103,22 +113,17 @@ func v1AuthRoutes(r *gin.Engine, mint *m.Mint) { return } - if unit != cashu.AUTH { - details := `You can only use "auth" tokens in this endpoint` - c.JSON(400, cashu.ErrorCodeToResponse(cashu.UNIT_NOT_SUPPORTED, &details)) + err = mint.CheckOutputSpent(tx, mintRequest.Outputs) + if err != nil { + slog.Warn("mint.VerifyOutputs(mintRequest.Outputs)", slog.Any("error", err)) + errorCode, details := utils.ParseErrorToCashuErrorCode(err) + c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - amountBlindMessages := uint64(0) - - for _, blindMessage := range mintRequest.Outputs { - amountBlindMessages += blindMessage.Amount - // check all blind messages have the same unit - } - - if amountBlindMessages > mint.Config.MINT_AUTH_MAX_BLIND_TOKENS { - slog.Warn("Trying to mint auth tokens over the limit") - c.JSON(400, cashu.ErrorCodeToResponse(cashu.MAXIMUM_BAT_MINT_LIMIT_EXCEEDED, nil)) + if unit != cashu.AUTH { + details := `You can only use "auth" tokens in this endpoint` + c.JSON(400, cashu.ErrorCodeToResponse(cashu.UNIT_NOT_SUPPORTED, &details)) return } @@ -137,7 +142,7 @@ func v1AuthRoutes(r *gin.Engine, mint *m.Mint) { return } - err = mint.MintDB.Commit(ctx, tx) + err = mint.MintDB.Commit(c.Request.Context(), tx) if err != nil { _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err)) return diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index f7f41526..c21c40b3 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -1,20 +1,12 @@ package routes import ( - "context" - "errors" - "fmt" "log/slog" - "strings" - "time" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5" "github.com/lescuer97/nutmix/api/cashu" m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" - "github.com/lightningnetwork/lnd/invoices" - "github.com/lightningnetwork/lnd/zpay32" ) func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { @@ -30,193 +22,27 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { return } - if mintRequest.Amount == 0 { - slog.Info("Amount missing") - c.JSON(400, "Amount missing") - return - } - - if mint.Config.PEG_OUT_ONLY { - slog.Info("Peg out only enables") - c.JSON(400, cashu.ErrorCodeToResponse(cashu.MINTING_DISABLED, nil)) - return - } - - if mint.Config.PEG_IN_LIMIT_SATS != nil { - if mintRequest.Amount > uint64(*mint.Config.PEG_IN_LIMIT_SATS) { - slog.Info("Mint amount over the limit", slog.Uint64("amount", mintRequest.Amount)) - - c.JSON(400, "Mint amount over the limit") - return - } - - } - - err = mint.VerifyUnitSupport(mintRequest.Unit) + response, err := mint.CreateMintQuote(c.Request.Context(), mintRequest, m.Bolt11) if err != nil { - slog.Warn("mint.VerifyUnitSupport(mintRequest.Unit)", slog.Any("error", err)) + slog.Info("mint.Swap(c.Request.Context(), swapRequest)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - expireTime := cashu.ExpiryTimeMinUnit(15) - now := time.Now().Unix() - - slog.Debug("Requesting invoice for amount", - slog.Uint64("amount", mintRequest.Amount), - slog.Any("backend", mint.LightningBackend.LightningType())) - - unit, err := cashu.UnitFromString(mintRequest.Unit) - - if err != nil { - slog.Warn("cashu.UnitFromString(mintRequest.Unit)", slog.Any("error", err), slog.Any("err", cashu.ErrUnitNotSupported)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - quoteId, err := utils.RandomHash() - if err != nil { - slog.Info("utils.RandomHash()", slog.Uint64("amount", mintRequest.Amount)) - c.JSON(500, "Opps! there was a problem with the mint") - return - } - - mintRequestDB := cashu.MintRequestDB{ - Quote: quoteId, - Expiry: expireTime, - Unit: unit.String(), - State: cashu.UNPAID, - SeenAt: now, - Amount: &mintRequest.Amount, - Pubkey: mintRequest.Pubkey, - Description: mintRequest.Description, - Request: "", - CheckingId: "", - Minted: false, - } - - resInvoice, err := mint.LightningBackend.RequestInvoice(mintRequestDB, cashu.NewAmount(unit, mintRequest.Amount)) - if err != nil { - slog.Info("Payment request", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - - if resInvoice.PaymentRequest == "" { - slog.Error("The lightning backend is not returning an invoice.") - c.JSON(500, "Opps!, something went wrong") - return - } - - mintRequestDB.Request = resInvoice.PaymentRequest - mintRequestDB.CheckingId = resInvoice.CheckingId - - ctx := context.Background() - tx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("rollback error", slog.Any("error", rollbackErr)) - } - } - }() - - err = mint.MintDB.SaveMintRequest(tx, mintRequestDB) - if err != nil { - slog.Error("SaveQuoteRequest", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - - err = mint.MintDB.Commit(ctx, tx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err)) - return - } - - res := mintRequestDB.PostMintQuoteBolt11Response() - c.JSON(200, res) + c.JSON(200, response) }) v1.GET("/mint/quote/bolt11/:quote", func(c *gin.Context) { quoteId := c.Param("quote") - - ctx := context.Background() - tx, err := mint.MintDB.GetTx(ctx) + response, err := mint.MintQuoteStatus(c.Request.Context(), quoteId, m.Bolt11) if err != nil { - _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("rollback error", slog.Any("error", rollbackErr)) - } - } - }() - - quote, err := mint.MintDB.GetMintRequestById(tx, quoteId) - if err != nil { - slog.Warn("mint:quote mint.MintDB.GetMintRequestById(tx, quoteId)", slog.Any("error", err)) + slog.Info("mint.MintQuoteStatus(c.Request.Context(), quoteId, m.Bolt11)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return - } - - err = mint.MintDB.Commit(ctx, tx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx stateChangeTX). %w", err)) - return - } - if quote.State == cashu.PAID || quote.State == cashu.ISSUED { - c.JSON(200, quote) - return - } - invoice, err := zpay32.Decode(quote.Request, mint.LightningBackend.GetNetwork()) - if err != nil { - slog.Warn("Mint decoding zpay32.Decode", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - quote, err = m.CheckMintRequest(mint, quote, invoice) - if err != nil { - slog.Warn("m.CheckMintRequest(mint, quote)", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - stateChangeTX, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, stateChangeTX); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("rollback error", slog.Any("error", rollbackErr)) - } - } - }() - - err = mint.MintDB.ChangeMintRequestState(stateChangeTX, quote.Quote, quote.State, quote.Minted) - if err != nil { - slog.Error("mint.MintDB.ChangeMintRequestState(tx, quote.Quote, quote.State, quote.Minted)", slog.Any("error", err)) - } - - err = mint.MintDB.Commit(ctx, stateChangeTX) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx stateChangeTX). %w", err)) - return - } - - res := quote.PostMintQuoteBolt11Response() - c.JSON(200, res) + c.JSON(200, response) }) v1.POST("/mint/bolt11", func(c *gin.Context) { @@ -229,215 +55,14 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - - keysets, err := mint.Signer.GetKeysets() - if err != nil { - slog.Warn(fmt.Errorf("mint.Signer.GetKeys(). %w", err).Error()) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - ctx := context.Background() - preparationTx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, preparationTx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("rollback error", slog.Any("error", rollbackErr)) - } - } - }() - - mintRequestDB, err := mint.MintDB.GetMintRequestById(preparationTx, mintRequest.Quote) - if err != nil { - slog.Warn(" mint-resquest mint.MintDB.GetMintRequestById(tx, mintRequest.Quote)", slog.Any("error", err)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - if mintRequestDB.Minted { - slog.Warn("Quote already minted", slog.String(utils.LogExtraInfo, mintRequestDB.Quote)) - c.JSON(400, cashu.ErrorCodeToResponse(cashu.QUOTE_ALREADY_ISSUED, nil)) - return - } - - if mintRequestDB.Pubkey.PublicKey != nil { - valid, err := mintRequest.VerifyPubkey(mintRequestDB.Pubkey.PublicKey) - if err != nil { - slog.Warn("Cold not verify signature", slog.Any("error", err)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - if !valid { - slog.Warn("Invalid signature", slog.Any("error", err)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - } - - err = mint.VerifyUnitSupport(mintRequestDB.Unit) - if err != nil { - slog.Warn("mint.VerifyUnitSupport(quote.Unit)", slog.Any("error", err)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - // quote - - slog.Debug(fmt.Sprintf("before verifying outputs for keysets %+v and mint quote id: %v. ", keysets.Keysets, mintRequestDB.Quote)) - _, err = mint.VerifyOutputs(preparationTx, mintRequest.Outputs, keysets.Keysets) - if err != nil { - slog.Warn("mint.VerifyOutputs(mintRequest.Outputs)", slog.Any("error", err)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - err = mint.MintDB.Commit(ctx, preparationTx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx, preparationTx). %w", err)) - return - } - - amountBlindMessages := uint64(0) - - for _, blindMessage := range mintRequest.Outputs { - amountBlindMessages += blindMessage.Amount - // check all blind messages have the same unit - } - - invoice, err := zpay32.Decode(mintRequestDB.Request, mint.LightningBackend.GetNetwork()) - if err != nil { - slog.Warn("Mint decoding zpay32.Decode", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - - cashuUnit, err := cashu.UnitFromString(mintRequestDB.Unit) - if err != nil { - slog.Warn("cashu.UnitFromString(mintRequestDB.Unit)", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - cashuBlindMessage := cashu.NewAmount(cashuUnit, amountBlindMessages) - err = cashuBlindMessage.To(cashu.Msat) - if err != nil { - _ = c.Error(fmt.Errorf("cashuBlindMessage.To(cashu.Msat). %w", err)) - return - } - - // check the amount in outputs are the same as the quote - if uint64(*invoice.MilliSat) < cashuBlindMessage.Amount { - slog.Info("wrong amount of milisats", slog.Int("invoice_milisats", int(*invoice.MilliSat)), slog.Int("needed_milisats", int(cashuBlindMessage.Amount))) - c.JSON(403, "Amounts in outputs are not the same") - return - } - if mintRequestDB.State == cashu.UNPAID { - - slog.Debug(fmt.Sprintf("Checking payment state quote id: %v. ", mintRequestDB.Quote)) - mintRequestDB, err = m.CheckMintRequest(mint, mintRequestDB, invoice) - if err != nil { - if errors.Is(err, invoices.ErrInvoiceNotFound) || strings.Contains(err.Error(), "NotFound") { - slog.Warn(fmt.Errorf(".CheckMintRequest(mint, mintRequestDB, invoice): %w", err).Error()) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - slog.Warn("m.CheckMintRequest(mint, quote)", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - afterCheckTx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, afterCheckTx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("rollback error", slog.Any("error", rollbackErr)) - } - } - }() - - slog.Debug(fmt.Sprintf("Changing state of mint request id %v. To: %v", mintRequestDB.Quote, mintRequestDB.State)) - err = mint.MintDB.ChangeMintRequestState(afterCheckTx, mintRequestDB.Quote, mintRequestDB.State, mintRequestDB.Minted) - if err != nil { - slog.Warn(fmt.Errorf("mint.MintDB.ChangeMintRequestState(afterCheckTx, quote.Quote, quote.State, quote.Minted): %w", err).Error()) - return - } - err = mint.MintDB.Commit(ctx, afterCheckTx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err)) - return - } - - } - - if mintRequestDB.State != cashu.PAID { - c.JSON(400, cashu.ErrorCodeToResponse(cashu.REQUEST_NOT_PAID, nil)) - return - } - slog.Debug(fmt.Sprintf("Signing blind signatures in Mint request : id %v", mintRequestDB.Quote)) - blindedSignatures, recoverySigsDb, err := mint.Signer.SignBlindMessages(mintRequest.Outputs) - if err != nil { - slog.Error("mint.Signer.SignBlindMessages(mintRequest.Outputs)", slog.Any("error", err)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - mintRequestDB.Minted = true - mintRequestDB.State = cashu.ISSUED - afterBlindSignTx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, afterBlindSignTx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("rollback error", slog.Any("error", rollbackErr)) - } - } - }() - - err = mint.MintDB.ChangeMintRequestState(afterBlindSignTx, mintRequestDB.Quote, mintRequestDB.State, mintRequestDB.Minted) + response, err := mint.Mint(c.Request.Context(), mintRequest, m.Bolt11) if err != nil { - slog.Error(fmt.Errorf("mint.MintDB.ChangeMintRequestState(afterBlindSignTx, quote.Quote, quote.State, quote.Minted): %w", err).Error()) - return - } - - slog.Debug(fmt.Sprintf("Saving restore sigs for quote: id %v", mintRequestDB.Quote)) - err = mint.MintDB.SaveRestoreSigs(afterBlindSignTx, recoverySigsDb) - if err != nil { - slog.Error(fmt.Errorf("mint.MintDB.SaveRestoreSigs(tx, recoverySigsDb): %w", err).Error()) + slog.Info("Incorrect body", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - - slog.Debug(fmt.Sprintf("Committing transaction for: id %v", mintRequestDB.Quote)) - err = mint.MintDB.Commit(ctx, afterBlindSignTx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx, afterBlindSignTx). %w", err)) - return - } - - slog.Debug("sending mint request to observer") - go mint.Observer.SendMintEvent(mintRequestDB) - slog.Debug(fmt.Sprintf("returning success to client: mint quote id: %v", mintRequestDB.Quote)) - // Store BlidedSignature - c.JSON(200, cashu.PostMintBolt11Response{ - Signatures: blindedSignatures, - }) + c.JSON(200, response) }) v1.POST("/melt/quote/bolt11", func(c *gin.Context) { @@ -450,160 +75,13 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { return } - err = mint.VerifyUnitSupport(meltRequest.Unit) + dbRequest, err := mint.MeltQuote(c.Request.Context(), meltRequest, m.Bolt11) if err != nil { - slog.Warn("mint.VerifyUnitSupport(quote.Unit)", slog.Any("error", err)) + slog.Warn("mint.MeltQuote", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - - invoice, err := zpay32.Decode(meltRequest.Request, mint.LightningBackend.GetNetwork()) - if err != nil { - slog.Info("zpay32.Decode", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - - if uint64(*invoice.MilliSat) == 0 { - c.JSON(400, "Invoice has no amount") - return - } - - if mint.Config.PEG_OUT_LIMIT_SATS != nil { - if int64(*invoice.MilliSat) > (int64(*mint.Config.PEG_OUT_LIMIT_SATS) * 1000) { - c.JSON(400, "Melt amount over the limit") - return - } - } - - quoteId, err := utils.RandomHash() - if err != nil { - slog.Info("utils.RandomHash()", slog.String(utils.LogExtraInfo, meltRequest.Request)) - c.JSON(500, "Opps! there was a problem with the mint") - return - } - - expireTime := cashu.ExpiryTimeMinUnit(15) - now := time.Now().Unix() - - unit, err := cashu.UnitFromString(meltRequest.Unit) - - if err != nil { - slog.Warn("cashu.UnitFromString(meltRequest.Unit)", slog.Any("error", err), slog.Any("err", cashu.ErrUnitNotSupported)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - invoiceAmountMilisats := uint64(*invoice.MilliSat) - cashuAmount := cashu.NewAmount(cashu.Msat, invoiceAmountMilisats) - err = cashuAmount.To(unit) - if err != nil { - slog.Error("cashuAmount.To(unit)", slog.Any("error", err), slog.Any("err", cashu.ErrUnitNotSupported)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - isMpp := false - mppAmount := cashu.NewAmount(unit, meltRequest.IsMpp()) - - // if mpp is valid than change amount to mpp amount - if mppAmount.Amount != 0 { - if mppAmount.Amount > cashuAmount.Amount { - slog.Warn("User tried to pay mpp amount bigger than the given invoice", slog.Any("invoiceAmount", cashuAmount.Amount), slog.Any("mppAmount", mppAmount.Amount)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - - } - isMpp = true - cashuAmount = mppAmount - } - - if isMpp && !mint.LightningBackend.ActiveMPP() { - slog.Info("Tried to do mpp when it is not available") - c.JSON(400, "Sorry! MPP is not available") - return - } - - // check if it's internal transaction if it is and it's mpp error out - - isInternal, err := mint.IsInternalTransaction(meltRequest.Request) - if err != nil { - slog.Info("mint.IsInternalTransaction(meltRequest.Request)", slog.Any("error", err)) - c.JSON(500, "Opps!, something went wrong") - return - } - - if isMpp && isInternal { - slog.Info("Internal MPP not allowed", slog.Any("error", err)) - c.JSON(403, "Internal MPP not allowed") - return - } - queryFee := uint64(0) - checkingId := quoteId - dbRequest := cashu.MeltRequestDB{ - Amount: cashuAmount.Amount, - Quote: quoteId, - Request: meltRequest.Request, - Unit: unit.String(), - Expiry: expireTime, - FeeReserve: (queryFee + 1), - State: cashu.UNPAID, - PaymentPreimage: "", - SeenAt: now, - Mpp: isMpp, - CheckingId: checkingId, - FeePaid: 0, - Melted: false, - } - - if !isInternal { - feesResponse, err := mint.LightningBackend.QueryFees(meltRequest.Request, invoice, isMpp, cashuAmount) - if err != nil { - slog.Info("mint.LightningBackend.QueryFees(meltRequest.Request, invoice, isMpp, cashuAmount)", slog.Any("error", err)) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - dbRequest.CheckingId = feesResponse.CheckingId - dbRequest.FeeReserve = feesResponse.Fees.Amount - dbRequest.Amount = feesResponse.AmountToSend.Amount - } - - ctx := context.Background() - tx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("m.MintDB.GetTx(ctx). %w", err)) - slog.Warn("m.MintDB.GetTx(ctx)", slog.Any("error", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("rollback error", slog.Any("error", rollbackErr)) - } - } - }() - - err = mint.MintDB.SaveMeltRequest(tx, dbRequest) - - if err != nil { - slog.Warn("SaveQuoteMeltRequest", slog.Any("error", err)) - slog.Warn("dbRequest", slog.Any("db_request", dbRequest)) - c.JSON(400, cashu.ErrorCodeToResponse(cashu.UNKNOWN, nil)) - return - } - - err = mint.MintDB.Commit(ctx, tx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err)) - slog.Warn("mint.MintDB.Commit(ctx tx)", slog.Any("error", err)) - return - } - c.JSON(200, dbRequest.GetPostMeltQuoteResponse()) }) @@ -631,7 +109,7 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { return } - quote, err := mint.Melt(c.Request.Context(), meltRequest) + quote, err := mint.Melt(c.Request.Context(), meltRequest, m.Bolt11) if err != nil { slog.Warn("mint.Melt(ctx, meltRequest)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) diff --git a/internal/routes/middleware/auth_test.go b/internal/routes/middleware/auth_test.go index 8d918477..9275bd28 100644 --- a/internal/routes/middleware/auth_test.go +++ b/internal/routes/middleware/auth_test.go @@ -102,7 +102,6 @@ func TestMintMatchPattern(t *testing.T) { if matches { t.Errorf(`This path should have not passed. "%s"`, "/v1/swap") } - } func TestMeltMatchPattern(t *testing.T) { @@ -205,5 +204,4 @@ func TestMeltMatchPattern(t *testing.T) { if matches { t.Errorf(`This path should have not passed. "%s"`, "/v1/swap") } - } diff --git a/internal/routes/middleware/cache.go b/internal/routes/middleware/cache.go index eb9ae224..2a84f004 100644 --- a/internal/routes/middleware/cache.go +++ b/internal/routes/middleware/cache.go @@ -31,7 +31,6 @@ var cachedPaths = map[string]bool{ func CacheMiddleware(store *persistence.InMemoryStore) gin.HandlerFunc { return func(c *gin.Context) { - if !cachedPaths[c.Request.URL.Path] { c.Next() return @@ -58,7 +57,8 @@ func CacheMiddleware(store *persistence.InMemoryStore) gin.HandlerFunc { c.Writer = w c.Next() if c.Writer.Status() == http.StatusOK { - if err := store.Set(cacheKey, w.body.Bytes(), 45*time.Minute); err != nil { + err := store.Set(cacheKey, w.body.Bytes(), 45*time.Minute) + if err != nil { slog.Warn("failed to set cache", slog.Any("error", err)) } } diff --git a/internal/routes/mint.go b/internal/routes/mint.go index 83f702c2..a6a91d3b 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -1,14 +1,9 @@ package routes import ( - "context" - "errors" - "fmt" "log/slog" - "time" "github.com/gin-gonic/gin" - "github.com/jackc/pgx/v5" "github.com/lescuer97/nutmix/api/cashu" m "github.com/lescuer97/nutmix/internal/mint" "github.com/lescuer97/nutmix/internal/utils" @@ -18,7 +13,6 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { v1 := r.Group("/v1") v1.GET("/keys", func(c *gin.Context) { - keys, err := mint.Signer.GetActiveKeys() if err != nil { slog.Error("mint.Signer.GetActiveKeys()", slog.Any("error", err)) @@ -27,11 +21,9 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { } c.JSON(200, keys) - }) v1.GET("/keys/:id", func(c *gin.Context) { - id := c.Param("id") keysets, err := mint.Signer.GetKeysById(id) @@ -43,10 +35,8 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { } c.JSON(200, keysets) - }) v1.GET("/keysets", func(c *gin.Context) { - keys, err := mint.Signer.GetKeysets() if err != nil { slog.Error("mint.Signer.GetKeys()", slog.Any("error", err)) @@ -58,205 +48,8 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { }) v1.GET("/info", func(c *gin.Context) { - - contacts := []cashu.ContactInfo{} - - email := mint.Config.EMAIL - - if len(email) > 0 { - contacts = append(contacts, cashu.ContactInfo{ - Method: "email", - Info: email, - }) - } - - nostr := mint.Config.NOSTR - - if len(nostr) > 0 { - contacts = append(contacts, cashu.ContactInfo{ - Method: "nostr", - Info: nostr, - }) - } - - nuts := make(map[string]any) - var baseNuts = []string{"1", "2", "3", "4", "5", "6"} - - var optionalNuts = []string{"7", "8", "9", "10", "11", "12", "17", "20"} - - if mint.LightningBackend.ActiveMPP() { - optionalNuts = append(optionalNuts, "15") - } - if mint.Config.MINT_REQUIRE_AUTH { - optionalNuts = append(optionalNuts, "21") - optionalNuts = append(optionalNuts, "22") - } - - for _, nut := range baseNuts { - b := false - - switch nut { - case "4": - bolt11Method := cashu.SwapMintMethod{ - Method: cashu.MethodBolt11, - Unit: cashu.Sat.String(), - MinAmount: 0, - MaxAmount: 0, - Options: nil, - Commands: nil, - } - - if mint.Config.PEG_IN_LIMIT_SATS != nil { - bolt11Method.MaxAmount = *mint.Config.PEG_IN_LIMIT_SATS - } - - descriptionEnabled := mint.LightningBackend.DescriptionSupport() - bolt11Method.Options = &cashu.SwapMintMethodOptions{ - Description: &descriptionEnabled, - } - - nuts[nut] = cashu.SwapMintInfo{ - Methods: &[]cashu.SwapMintMethod{ - bolt11Method, - }, - Disabled: &b, - Supported: nil, - } - if entry, ok := nuts[nut]; ok { - mintInfo, ok := entry.(cashu.SwapMintInfo) - if !ok { - slog.Error("nuts entry type mismatch", slog.String("nut", nut), slog.Any("value", entry)) - c.JSON(500, "Server side error") - return - } - // Then we modify the copy - mintInfo.Disabled = &mint.Config.PEG_OUT_ONLY - - // Then we reassign map entry - nuts[nut] = mintInfo - } - - case "5": - bolt11Method := cashu.SwapMintMethod{ - Method: cashu.MethodBolt11, - Unit: cashu.Sat.String(), - MinAmount: 0, - MaxAmount: 0, - Options: nil, - Commands: nil, - } - - if mint.Config.PEG_OUT_LIMIT_SATS != nil { - bolt11Method.MaxAmount = *mint.Config.PEG_OUT_LIMIT_SATS - } - - nuts[nut] = cashu.SwapMintInfo{ - Methods: &[]cashu.SwapMintMethod{ - bolt11Method, - }, - Disabled: &b, - Supported: nil, - } - - default: - nuts[nut] = cashu.SwapMintInfo{ - Disabled: &b, - Methods: nil, - Supported: nil, - } - - } - } - - for _, nut := range optionalNuts { - b := true - switch nut { - case "15": - bolt11Method := cashu.SwapMintMethod{ - Method: cashu.MethodBolt11, - Unit: cashu.Sat.String(), - MinAmount: 0, - MaxAmount: 0, - Options: nil, - Commands: nil, - } - - nuts[nut] = cashu.SwapMintInfo{ - Methods: &[]cashu.SwapMintMethod{ - bolt11Method, - }, - Disabled: nil, - Supported: nil, - } - case "17": - - wsMethod := make(map[string][]cashu.SwapMintMethod) - - bolt11Method := cashu.SwapMintMethod{ - Method: cashu.MethodBolt11, - Unit: cashu.Sat.String(), - MinAmount: 0, - MaxAmount: 0, - Options: nil, - Commands: []cashu.SubscriptionKind{ - cashu.Bolt11MeltQuote, - cashu.Bolt11MintQuote, - cashu.ProofStateWs, - }, - } - wsMethod["supported"] = []cashu.SwapMintMethod{bolt11Method} - - nuts[nut] = wsMethod - - case "20": - wsMethod := make(map[string]bool) - - wsMethod["supported"] = true - - nuts[nut] = wsMethod - - case "21": - formatedDiscoveryUrl := mint.Config.MINT_AUTH_OICD_URL + "/.well-known/openid-configuration" - protectedRoutes := cashu.Nut21Info{ - OpenIdDiscovery: formatedDiscoveryUrl, - ClientId: mint.Config.MINT_AUTH_OICD_CLIENT_ID, - ProtectedRoutes: cashu.ConvertRouteListToProtectedRouteList(mint.Config.MINT_AUTH_CLEAR_AUTH_URLS), - } - - nuts[nut] = protectedRoutes - case "22": - protectedRoutes := cashu.Nut22Info{ - BatMaxMint: mint.Config.MINT_AUTH_MAX_BLIND_TOKENS, - ProtectedRoutes: cashu.ConvertRouteListToProtectedRouteList(mint.Config.MINT_AUTH_BLIND_AUTH_URLS), - } - - nuts[nut] = protectedRoutes - - default: - nuts[nut] = cashu.SwapMintInfo{ - Supported: &b, - Methods: nil, - Disabled: nil, - } - - } - } - - response := cashu.GetInfoResponse{ - Name: mint.Config.NAME, - Version: "nutmix/" + utils.AppVersion, - Pubkey: mint.MintPubkey, - Description: mint.Config.DESCRIPTION, - DescriptionLong: mint.Config.DESCRIPTION_LONG, - Motd: mint.Config.MOTD, - Contact: contacts, - Nuts: nuts, - IconUrl: mint.Config.IconUrl, - TosUrl: mint.Config.TosUrl, - Time: time.Now().Unix(), - } - - c.JSON(200, response) + info := mint.Info() + c.JSON(200, info) }) v1.POST("/swap", func(c *gin.Context) { @@ -270,168 +63,13 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { return } - if len(swapRequest.Inputs) == 0 || len(swapRequest.Outputs) == 0 { - slog.Info("Inputs or Outputs are empty") - c.JSON(400, "Inputs or Outputs are empty") - return - } - - _, SecretsList, err := utils.GetAndCalculateProofsValues(&swapRequest.Inputs) - if err != nil { - slog.Warn("utils.GetAndCalculateProofsValues(&swapRequest.Inputs)", slog.Any("error", err)) - c.JSON(400, "Problem processing proofs") - return - } - - // Verify spending conditions - EXCLUSIVE paths following CDK pattern - hasSigAll, err := cashu.ProofsHaveSigAll(swapRequest.Inputs) - if err != nil { - slog.Warn(fmt.Errorf("cashu.ProofsHaveSigAll(swapRequest.Inputs). %w", err).Error()) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - if hasSigAll { - // SIG_ALL path: verify all conditions match and signature is valid against combined message - err = swapRequest.ValidateSigflag() - if err != nil { - slog.Warn(fmt.Errorf("swapRequest.ValidateSigflag(). %w", err).Error()) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - } else { - // Individual verification path: verify each proof's P2PK/HTLC spend conditions - err = cashu.VerifyProofsSpendConditions(swapRequest.Inputs) - if err != nil { - slog.Warn(fmt.Errorf("mint.VerifyProofsSpendConditions(swapRequest.Inputs). %w", err).Error()) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - } - - // Always verify BDHKE cryptographic signatures (regardless of SIG_ALL) - err = mint.VerifyProofsBDHKE(swapRequest.Inputs) + response, err := mint.Swap(c.Request.Context(), swapRequest) if err != nil { - slog.Warn(fmt.Errorf("mint.VerifyProofsBDHKE(swapRequest.Inputs). %w", err).Error()) + slog.Info("mint.Swap(c.Request.Context(), swapRequest)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - - ctx := context.Background() - preparationTx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, preparationTx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("could not swap state", slog.Any("error", rollbackErr)) - } - } - }() - - // VerifyInputsAndOutputs now only checks balance/unit and BDHKE (spend conditions already verified) - err = mint.VerifyInputsAndOutputs(preparationTx, swapRequest.Inputs, swapRequest.Outputs) - if err != nil { - slog.Warn(fmt.Errorf("mint.VerifyInputsAndOutputs(swapRequest.Inputs, swapRequest.Outputs). %w", err).Error()) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - // check if we know any of the proofs - knownProofs, err := mint.MintDB.GetProofsFromSecretCurve(preparationTx, SecretsList) - - if err != nil { - slog.Error("mint.MintDB.GetProofsFromSecretCurve(tx, SecretsList)", slog.String(utils.LogExtraInfo, err.Error())) - c.JSON(400, cashu.ErrorCodeToResponse(cashu.UNKNOWN, nil)) - return - } - - if len(knownProofs) != 0 { - slog.Debug("Proofs already spent", slog.Any("known_proofs", knownProofs)) - for _, p := range knownProofs { - if p.State == cashu.PROOF_PENDING { - c.JSON(400, cashu.ErrorCodeToResponse(cashu.PROOFS_PENDING, nil)) - return - } - } - - c.JSON(400, cashu.ErrorCodeToResponse(cashu.PROOF_ALREADY_SPENT, nil)) - return - } - - swapRequest.Inputs.SetProofsState(cashu.PROOF_PENDING) - - // send proofs to database - err = mint.MintDB.SaveProof(preparationTx, swapRequest.Inputs) - - if err != nil { - slog.Error("mint.MintDB.SaveProof(tx, swapRequest.Inputs)", slog.String(utils.LogExtraInfo, err.Error())) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(403, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - err = mint.MintDB.Commit(ctx, preparationTx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err)) - return - } - - // sign the outputs - blindedSignatures, recoverySigsDb, err := mint.Signer.SignBlindMessages(swapRequest.Outputs) - if err != nil { - slog.Error("mint.Signer.SignBlindMessages(swapRequest.Outputs)", slog.String(utils.LogExtraInfo, err.Error())) - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - response := cashu.PostSwapResponse{ - Signatures: blindedSignatures, - } - afterSigningTx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err)) - return - } - defer func() { - if rollbackErr := mint.MintDB.Rollback(ctx, afterSigningTx); rollbackErr != nil { - if !errors.Is(rollbackErr, pgx.ErrTxClosed) { - slog.Warn("could not swap state", slog.Any("error", rollbackErr)) - } - } - }() - - swapRequest.Inputs.SetProofsState(cashu.PROOF_SPENT) - err = mint.MintDB.SetProofsState(afterSigningTx, swapRequest.Inputs, cashu.PROOF_SPENT) - if err != nil { - slog.Warn("mint.MintDB.SetProofsState(tx,swapRequest.Inputs , cashu.PROOF_SPENT)", slog.Any("error", err)) - - errorCode, details := utils.ParseErrorToCashuErrorCode(err) - c.JSON(403, cashu.ErrorCodeToResponse(errorCode, details)) - return - } - - err = mint.MintDB.SaveRestoreSigs(afterSigningTx, recoverySigsDb) - if err != nil { - slog.Error("database.SetRestoreSigs", slog.String(utils.LogExtraInfo, err.Error())) - slog.Error("recoverySigsDb", slog.Any("recovery_sigs", recoverySigsDb)) - c.JSON(200, response) - return - } - err = mint.MintDB.Commit(ctx, afterSigningTx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx, afterSigningTx). %w", err)) - return - } - go mint.Observer.SendProofsEvent(swapRequest.Inputs) c.JSON(200, response) }) @@ -458,8 +96,8 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { checkStateResponse.States = states c.JSON(200, checkStateResponse) - }) + v1.POST("/restore", func(c *gin.Context) { var restoreRequest cashu.PostRestoreRequest err := c.BindJSON(&restoreRequest) @@ -470,44 +108,14 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { return } - blindingFactors := []cashu.WrappedPublicKey{} - - for _, output := range restoreRequest.Outputs { - blindingFactors = append(blindingFactors, output.B_) - } - - ctx := context.Background() - tx, err := mint.MintDB.GetTx(ctx) - if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.GetTx(ctx): %w", err)) - return - } - blindRecoverySigs, err := mint.MintDB.GetRestoreSigsFromBlindedMessages(tx, blindingFactors) - if err != nil { - slog.Error("mint.MintDB.GetRestoreSigsFromBlindedMessages(tx, blindingFactors)", slog.String(utils.LogExtraInfo, err.Error())) - c.JSON(500, "Opps!, something went wrong") - return - } - err = mint.MintDB.Commit(ctx, tx) + response, err := mint.Restore(c.Request.Context(), restoreRequest) if err != nil { - _ = c.Error(fmt.Errorf("mint.MintDB.Commit(ctx tx). %w", err)) + slog.Info("mint.Restore(c.Request.Context(), restoreRequest)", slog.Any("error", err)) + errorCode, details := utils.ParseErrorToCashuErrorCode(err) + c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - restoredBlindSigs := []cashu.BlindSignature{} - restoredBlindMessage := []cashu.BlindedMessage{} - - for _, sigRecover := range blindRecoverySigs { - restoredSig, restoredMessage := sigRecover.GetSigAndMessage() - restoredBlindSigs = append(restoredBlindSigs, restoredSig) - restoredBlindMessage = append(restoredBlindMessage, restoredMessage) - } - - c.JSON(200, cashu.PostRestoreResponse{ - Outputs: restoredBlindMessage, - Signatures: restoredBlindSigs, - Promises: restoredBlindSigs, - }) + c.JSON(200, response) }) - } diff --git a/internal/routes/routes.go b/internal/routes/routes.go index bbf2c095..f355822f 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -13,5 +13,4 @@ func V1Routes(r *gin.Engine, mint *mint.Mint) { v1MintRoutes(r, mint) v1bolt11Routes(r, mint) v1WebSocketRoute(r, mint) - } diff --git a/internal/routes/websocket.go b/internal/routes/websocket.go index f20edbe0..318ba709 100644 --- a/internal/routes/websocket.go +++ b/internal/routes/websocket.go @@ -38,7 +38,8 @@ func v1WebSocketRoute(r *gin.Engine, mint *m.Mint) { return } defer func() { - if err := conn.Close(); err != nil { + err := conn.Close() + if err != nil { slog.Warn("failed to close websocket connection", slog.Any("error", err)) } }() @@ -157,7 +158,6 @@ func v1WebSocketRoute(r *gin.Engine, mint *m.Mint) { return } } - }) } @@ -180,7 +180,6 @@ func handleWSRequest(request cashu.WsRequest, observer *m.Observer, proofChan ch for _, filter := range request.Params.Filters { observer.AddMeltWatch(filter, m.MeltQuoteChannel{Channel: meltChan, SubId: request.Params.SubId}) } - } case cashu.Unsubcribe: @@ -217,7 +216,6 @@ func ListenToIncommingMessage(subs *m.Observer, conn *websocket.Conn, listenChan } func CheckStatusOfSub(ctx context.Context, request cashu.WsRequest, mint *m.Mint, conn *websocket.Conn) error { - statusNotif := cashu.WsNotification{ JsonRpc: "2.0", Method: cashu.Subcribe, @@ -240,7 +238,8 @@ func CheckStatusOfSub(ctx context.Context, request cashu.WsRequest, mint *m.Mint } defer func() { if err != nil { - if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { + rollbackErr := mint.MintDB.Rollback(ctx, tx) + if rollbackErr != nil { slog.Warn("rollback error", slog.Any("error", rollbackErr)) } } @@ -354,7 +353,6 @@ func CheckStatusOfSub(ctx context.Context, request cashu.WsRequest, mint *m.Mint } } } - } return nil } diff --git a/internal/signer/local_signer/derivation.go b/internal/signer/local_signer/derivation.go index 478f6e39..f933c51c 100644 --- a/internal/signer/local_signer/derivation.go +++ b/internal/signer/local_signer/derivation.go @@ -16,7 +16,7 @@ import ( ) func GenerateKeysets(versionKey *hdkeychain.ExtendedKey, seed cashu.Seed) ([]cashu.MintKey, error) { - var keysets []cashu.MintKey + var keysets = make([]cashu.MintKey, len(seed.Amounts)) // Get the current time currentTime := time.Now() @@ -49,7 +49,7 @@ func GenerateKeysets(versionKey *hdkeychain.ExtendedKey, seed cashu.Seed) ([]cas FinalExpiry: nil, } - keysets = append(keysets, keyset) + keysets[i] = keyset } return keysets, nil @@ -90,7 +90,6 @@ func sortPubkeyMapToOrganizedArray(pubkeyMap map[uint64]*btcec.PublicKey) []pubk return int(a.Amount) - int(b.Amount) }) return arrayPubkeys - } func generateKeysetV2Preimage(sortedPubkeyArray []pubkeyWithAmount, unit string, fee uint, finalExpiry *time.Time) string { @@ -146,10 +145,8 @@ func getDerivationSteps(path string) ([]uint32, error) { derivationPaths := make([]uint32, len(derivationPathSeparation)) for i := range derivationPathSeparation { - splitDer := strings.Split(derivationPathSeparation[i], "'") if len(splitDer) == 2 { - derIndex, err := strconv.ParseUint(splitDer[0], 10, 32) if err != nil { return nil, fmt.Errorf("could not convert derivation path. %w", err) @@ -165,15 +162,12 @@ func getDerivationSteps(path string) ([]uint32, error) { } derivationPaths[i] = uint32(derIndex) continue - } return derivationPaths, nil - } func deriveSeed(seed cashu.Seed, mintKey *hdkeychain.ExtendedKey) ([]cashu.MintKey, error) { - if seed.Legacy { legacyKey, err := legacyGetMintPrivateKey() if err != nil { @@ -223,7 +217,6 @@ func GetKeysetsFromSeeds(seeds []cashu.Seed, mintKey *hdkeychain.ExtendedKey) (m newSeedId = DeriveKeysetIdV2(pubkeysWithValues, seed.Unit, seed.InputFeePpk, finalExpiry) default: return nil, nil, fmt.Errorf("could not generate a seed id") - } if newSeedId != seed.Id { @@ -242,5 +235,4 @@ func GetKeysetsFromSeeds(seeds []cashu.Seed, mintKey *hdkeychain.ExtendedKey) (m newKeysets[seed.Id] = mintkeyMap } return newKeysets, newActiveKeysets, nil - } diff --git a/internal/signer/local_signer/legacy.go b/internal/signer/local_signer/legacy.go index 0d5ad389..1d720d4d 100644 --- a/internal/signer/local_signer/legacy.go +++ b/internal/signer/local_signer/legacy.go @@ -37,7 +37,6 @@ func legacyGetMintPrivateKey() (*bip32.Key, error) { return nil, fmt.Errorf(" bip32.NewMasterKey(privateKey.Serialize()). %w", err) } return masterKey, nil - } func legacyDeriveKeyset(mintKey *bip32.Key, seed cashu.Seed) ([]cashu.MintKey, error) { @@ -49,7 +48,6 @@ func legacyDeriveKeyset(mintKey *bip32.Key, seed cashu.Seed) ([]cashu.MintKey, e unitKey, err := mintKey.NewChildKey(uint32(unit.EnumIndex())) if err != nil { - return nil, fmt.Errorf("mintKey.NewChildKey(uint32(unit.EnumIndex())). %w", err) } @@ -67,7 +65,7 @@ func legacyDeriveKeyset(mintKey *bip32.Key, seed cashu.Seed) ([]cashu.MintKey, e } func legacyGenerateKeysets(versionKey *bip32.Key, seed cashu.Seed) ([]cashu.MintKey, error) { - var keysets []cashu.MintKey + var keysets = make([]cashu.MintKey, len(seed.Amounts)) // Get the current time currentTime := time.Now() @@ -93,7 +91,7 @@ func legacyGenerateKeysets(versionKey *bip32.Key, seed cashu.Seed) ([]cashu.Mint FinalExpiry: nil, } - keysets = append(keysets, keyset) + keysets[i] = keyset } return keysets, nil diff --git a/internal/signer/local_signer/local_signer_test.go b/internal/signer/local_signer/local_signer_test.go index 6abd5527..3dfbdf7d 100644 --- a/internal/signer/local_signer/local_signer_test.go +++ b/internal/signer/local_signer/local_signer_test.go @@ -42,7 +42,6 @@ func TestRotateUnexistingSeedUnit(t *testing.T) { keys, err := localsigner.GetKeysets() if err != nil { t.Fatalf("localsigner.GetKeys() %+v", err) - } if len(keys.Keysets) != 3 { t.Errorf("Version should be 3. it's %v", len(keys.Keysets)) @@ -122,10 +121,8 @@ func TestRotateAuthSeedUnit(t *testing.T) { keys, err := localsigner.GetAuthActiveKeys() if err != nil { t.Fatalf("localsigner.GetKeys() %+v", err) - } if len(keys.Keysets) != 1 { - t.Errorf("There should only be one keyset for auth. there is: %v", len(keys.Keysets)) } @@ -324,7 +321,6 @@ func TestAuthConvertToInteger(t *testing.T) { if intRef != 1222349093 { t.Errorf("auth bytes are wrong %v", intRef) } - } func TestDeriveKeysetSat(t *testing.T) { @@ -509,7 +505,6 @@ func TestDeriveKeysetAuth(t *testing.T) { if id != "00e1cf6079abb988" { t.Errorf("id was incorrect. %v", id) } - } func TestLoadLegacyAndNonLegacySeeds(t *testing.T) { diff --git a/internal/signer/local_signer/signatory.go b/internal/signer/local_signer/signatory.go index 9591a00b..21d01d3f 100644 --- a/internal/signer/local_signer/signatory.go +++ b/internal/signer/local_signer/signatory.go @@ -31,5 +31,4 @@ func unitNormalization(unit string) string { unitStr = norm.NFC.String(unitStr) // Convert the normalized string to uppercase using Unicode-aware semantics return strings.ToUpper(unitStr) - } diff --git a/internal/signer/local_signer/signer.go b/internal/signer/local_signer/signer.go index 6f3409e9..2c299e9a 100644 --- a/internal/signer/local_signer/signer.go +++ b/internal/signer/local_signer/signer.go @@ -61,7 +61,6 @@ func SetupLocalSigner(db database.MintDB) (LocalSigner, error) { return localsigner, fmt.Errorf("db.SaveNewSeeds([]cashu.Seed{newSeed}). %w", err) } seeds = append(seeds, newSeed) - } keysets, activeKeysets, err := GetKeysetsFromSeeds(seeds, masterKey) if err != nil { @@ -96,7 +95,6 @@ func (l *LocalSigner) GetActiveKeys() (signer.GetKeysResponse, error) { } func (l *LocalSigner) GetKeysById(id string) (signer.GetKeysResponse, error) { - val, exists := l.keysets[id] if exists { var keys []cashu.MintKey @@ -107,7 +105,6 @@ func (l *LocalSigner) GetKeysById(id string) (signer.GetKeysResponse, error) { } return signer.OrderKeysetByUnit(keys), nil - } return signer.GetKeysResponse{}, signer.ErrNoKeysetFound } @@ -186,11 +183,9 @@ func (l *LocalSigner) createNewSeed(mintPrivateKey *hdkeychain.ExtendedKey, unit if final_expiry != nil { timestamp := uint64(final_expiry.Unix()) newSeed.FinalExpiry = ×tamp - } return newSeed, nil - } func (l *LocalSigner) RotateKeyset(unit cashu.Unit, fee uint, expiry_limit_hours uint) error { @@ -200,7 +195,8 @@ func (l *LocalSigner) RotateKeyset(unit cashu.Unit, fee uint, expiry_limit_hours return fmt.Errorf("l.db.GetTx(ctx). %w", err) } defer func() { - if err := l.db.Rollback(ctx, tx); err != nil { + err := l.db.Rollback(ctx, tx) + if err != nil { if !errors.Is(err, pgx.ErrTxClosed) { slog.Warn("rotate keyset sql transaction error", slog.Any("error", err)) } @@ -213,7 +209,6 @@ func (l *LocalSigner) RotateKeyset(unit cashu.Unit, fee uint, expiry_limit_hours if err != nil { if errors.Is(err, pgx.ErrNoRows) { return fmt.Errorf("database.GetSeedsByUnit(tx, unit). %w", err) - } } @@ -280,10 +275,10 @@ func (l *LocalSigner) RotateKeyset(unit cashu.Unit, fee uint, expiry_limit_hours } func (l *LocalSigner) SignBlindMessages(messages []cashu.BlindedMessage) ([]cashu.BlindSignature, []cashu.RecoverSigDB, error) { - var blindedSignatures []cashu.BlindSignature - var recoverSigDB []cashu.RecoverSigDB + var blindedSignatures = make([]cashu.BlindSignature, len(messages)) + var recoverSigDB = make([]cashu.RecoverSigDB, len(messages)) - for _, output := range messages { + for i, output := range messages { correctKeyset := l.activeKeysets[output.Id][output.Amount] if correctKeyset.PrivKey == nil || !correctKeyset.Active { @@ -307,16 +302,13 @@ func (l *LocalSigner) SignBlindMessages(messages []cashu.BlindedMessage) ([]cash return nil, nil, err } - blindedSignatures = append(blindedSignatures, blindSignature) - recoverSigDB = append(recoverSigDB, recoverySig) - + blindedSignatures[i] = blindSignature + recoverSigDB[i] = recoverySig } return blindedSignatures, recoverSigDB, nil - } func (l *LocalSigner) VerifyProofs(proofs []cashu.Proof) error { - for _, proof := range proofs { err := l.validateProof(proof) if err != nil { @@ -352,7 +344,6 @@ func (l *LocalSigner) validateProof(proof cashu.Proof) error { } return nil - } func (l *LocalSigner) GetSignerPubkey() (string, error) { return hex.EncodeToString(l.pubkey.SerializeCompressed()), nil @@ -378,7 +369,6 @@ func (l *LocalSigner) GetAuthActiveKeys() (signer.GetKeysResponse, error) { } func (l *LocalSigner) GetAuthKeysById(id string) (signer.GetKeysResponse, error) { - val, exists := l.keysets[id] if exists { var keys []cashu.MintKey @@ -389,7 +379,6 @@ func (l *LocalSigner) GetAuthKeysById(id string) (signer.GetKeysResponse, error) } return signer.OrderKeysetByUnit(keys), nil - } return signer.GetKeysResponse{}, signer.ErrNoKeysetFound } diff --git a/internal/signer/remote_signer/remote_signer.go b/internal/signer/remote_signer/remote_signer.go index 5ad08972..52858f0c 100644 --- a/internal/signer/remote_signer/remote_signer.go +++ b/internal/signer/remote_signer/remote_signer.go @@ -83,7 +83,6 @@ func clientVersionInterceptor() grpc.UnaryClientInterceptor { // gets all active keys func (s *RemoteSigner) setupSignerPubkeys() error { - ctx := context.Background() emptyRequest := sig.EmptyRequest{} @@ -161,9 +160,11 @@ func (s *RemoteSigner) setupSignerPubkeys() error { // gets all active keys func (s *RemoteSigner) GetActiveKeys() (signer.GetKeysResponse, error) { - var keys []MintPublicKeyset + keys := make([]MintPublicKeyset, len(s.activeKeysets)) + indexActiveKeysets := 0 for _, keyset := range s.activeKeysets { - keys = append(keys, keyset) + keys[indexActiveKeysets] = keyset + indexActiveKeysets++ } return OrderKeysetByUnit(keys), nil } @@ -178,7 +179,6 @@ func (s *RemoteSigner) GetKeysById(id string) (signer.GetKeysResponse, error) { // gets all keys from the signer func (s *RemoteSigner) GetKeysets() (signer.GetKeysetsResponse, error) { - var response signer.GetKeysetsResponse for _, seed := range s.keysets { if seed.Unit != cashu.AUTH.String() { @@ -196,7 +196,6 @@ func (s *RemoteSigner) GetKeysets() (signer.GetKeysetsResponse, error) { } func (s *RemoteSigner) RotateKeyset(unit cashu.Unit, fee uint, expiry_limit_hours uint) error { - ctx := context.Background() unitSig, err := ConvertCashuUnitToSignature(unit) @@ -241,7 +240,6 @@ func (s *RemoteSigner) RotateKeyset(unit cashu.Unit, fee uint, expiry_limit_hour } func (s *RemoteSigner) SignBlindMessages(messages []cashu.BlindedMessage) ([]cashu.BlindSignature, []cashu.RecoverSigDB, error) { - ctx := context.Background() blindedMessageRequest := sig.BlindedMessages{ BlindedMessages: make([]*sig.BlindedMessage, len(messages)), @@ -293,21 +291,18 @@ func (s *RemoteSigner) SignBlindMessages(messages []cashu.BlindedMessage) ([]cas Dleq: val.Dleq, MeltQuote: "", }) - } return blindSigs, recoverySigs, nil } func (s *RemoteSigner) VerifyProofs(proofs []cashu.Proof) error { - ctx := context.Background() // INFO: we verify locally if the proofs are locked and valid before sending to the crypto signer proofsVericationRequest := sig.Proofs{ Proof: make([]*sig.Proof, len(proofs)), } for i, val := range proofs { - C := val.C.SerializeCompressed() bytesId, err := hex.DecodeString(val.Id) @@ -343,7 +338,6 @@ func (s *RemoteSigner) VerifyProofs(proofs []cashu.Proof) error { } func (s *RemoteSigner) GetSignerPubkey() (string, error) { - return hex.EncodeToString(s.pubkey), nil } @@ -364,7 +358,6 @@ func (l *RemoteSigner) GetAuthActiveKeys() (signer.GetKeysResponse, error) { } func (s *RemoteSigner) GetAuthKeysById(id string) (signer.GetKeysResponse, error) { - val, exists := s.keysets[id] if exists { if val.Unit == cashu.AUTH.String() { diff --git a/internal/signer/remote_signer/transformer.go b/internal/signer/remote_signer/transformer.go index a8938cfc..b6c2559b 100644 --- a/internal/signer/remote_signer/transformer.go +++ b/internal/signer/remote_signer/transformer.go @@ -21,7 +21,8 @@ func ConvertSigBlindSignaturesToCashuBlindSigs(sigs *sig.BlindSignResponse) ([]c return blindSigs, errors.New("signer response is nil") } - if err := signerValidator.Struct(sigs); err != nil { + err := signerValidator.Struct(sigs) + if err != nil { return blindSigs, fmt.Errorf("signer response validation failed: %w", err) } @@ -32,7 +33,8 @@ func ConvertSigBlindSignaturesToCashuBlindSigs(sigs *sig.BlindSignResponse) ([]c return blindSigs, fmt.Errorf("signer signature validation failed at index %d: %w", i, err) } if val.Dleq != nil { - if err := signerValidator.Struct(val.Dleq); err != nil { + err := signerValidator.Struct(val.Dleq) + if err != nil { return blindSigs, fmt.Errorf("signer signature dleq validation failed at index %d: %w", i, err) } } @@ -99,7 +101,8 @@ func ConvertSigUnitToCashuUnit(sigUnit *sig.CurrencyUnit) (cashu.Unit, error) { if sigUnit == nil { return cashu.Sat, errors.New("signer currency unit is nil") } - if err := signerValidator.Struct(sigUnit); err != nil { + err := signerValidator.Struct(sigUnit) + if err != nil { return cashu.Sat, fmt.Errorf("signer currency unit validation failed: %w", err) } diff --git a/internal/signer/remote_signer/util.go b/internal/signer/remote_signer/util.go index 4a1277cc..8e12d9e1 100644 --- a/internal/signer/remote_signer/util.go +++ b/internal/signer/remote_signer/util.go @@ -12,7 +12,6 @@ import ( ) func GetTlsSecurityCredential() (credentials.TransportCredentials, error) { - tlsCertPath := os.Getenv("SIGNER_CLIENT_TLS_CERT") if tlsCertPath == "" { log.Panic("SIGNER_CLIENT_TLS_KEY path not available.") @@ -55,7 +54,6 @@ func GetTlsSecurityCredential() (credentials.TransportCredentials, error) { // Create the TLS credentials creds := credentials.NewTLS(tlsConfig) return creds, nil - } func OrderKeysetByUnit(keysets []MintPublicKeyset) signer.GetKeysResponse { var typesOfUnits = make(map[string][]MintPublicKeyset) diff --git a/internal/signer/utils.go b/internal/signer/utils.go index a4bc5e73..fe472e14 100644 --- a/internal/signer/utils.go +++ b/internal/signer/utils.go @@ -30,12 +30,10 @@ func OrderKeysetByUnit(keysets []cashu.MintKey) GetKeysResponse { keysetResponse.InputFeePpk = value[0].InputFeePpk for _, keyset := range value { - keysetResponse.Keys[keyset.Amount] = hex.EncodeToString(keyset.PrivKey.PubKey().SerializeCompressed()) } res.Keysets = append(res.Keysets, keysetResponse) } return res - } diff --git a/internal/utils/common.go b/internal/utils/common.go index e1ab66f3..92fc0c3e 100644 --- a/internal/utils/common.go +++ b/internal/utils/common.go @@ -27,7 +27,6 @@ const CLNGRPC LightningBackend = "ClnGrpcWallet" const Strike LightningBackend = "Strike" func StringToLightningBackend(text string) LightningBackend { - switch text { case string(FAKE_WALLET): return FAKE_WALLET @@ -39,7 +38,6 @@ func StringToLightningBackend(text string) LightningBackend { return Strike default: return FAKE_WALLET - } } @@ -141,7 +139,6 @@ func (c *Config) UseEnviromentVars() { c.MINT_LNBITS_ENDPOINT = os.Getenv("MINT_LNBITS_ENDPOINT") c.MINT_LNBITS_KEY = os.Getenv("MINT_LNBITS_KEY") - } func RandomHash() (string, error) { // Create a byte slice of 30 random bytes diff --git a/internal/utils/files.go b/internal/utils/files.go index 924750d7..63054f09 100644 --- a/internal/utils/files.go +++ b/internal/utils/files.go @@ -40,7 +40,8 @@ func ParseLogFileByLevelAndTime(file *os.File, wantedLevel []slog.Level, limitTi // unmarshal generically first var raw map[string]json.RawMessage - if err := json.Unmarshal(lineBytes, &raw); err != nil { + err := json.Unmarshal(lineBytes, &raw) + if err != nil { continue } @@ -49,7 +50,8 @@ func ParseLogFileByLevelAndTime(file *os.File, wantedLevel []slog.Level, limitTi // extract time (try RFC3339 string or unix int) if tRaw, ok := raw["time"]; ok { var tStr string - if err := json.Unmarshal(tRaw, &tStr); err == nil { + err := json.Unmarshal(tRaw, &tStr) + if err == nil { // try common timestamp layouts if parsed, err := time.Parse(time.RFC3339, tStr); err == nil { logRecord.Time = parsed @@ -58,7 +60,8 @@ func ParseLogFileByLevelAndTime(file *os.File, wantedLevel []slog.Level, limitTi } } else { var unix int64 - if err := json.Unmarshal(tRaw, &unix); err == nil { + err := json.Unmarshal(tRaw, &unix) + if err == nil { logRecord.Time = time.Unix(unix, 0) } } @@ -74,7 +77,8 @@ func ParseLogFileByLevelAndTime(file *os.File, wantedLevel []slog.Level, limitTi // extract level if lRaw, ok := raw["level"]; ok { var lStr string - if err := json.Unmarshal(lRaw, &lStr); err == nil { + err := json.Unmarshal(lRaw, &lStr) + if err == nil { switch strings.ToLower(lStr) { case "debug": logRecord.Level = slog.LevelDebug @@ -92,7 +96,8 @@ func ParseLogFileByLevelAndTime(file *os.File, wantedLevel []slog.Level, limitTi // keep existing extra-info if present if eiRaw, ok := raw["extra-info"]; ok { var eiStr string - if err := json.Unmarshal(eiRaw, &eiStr); err == nil { + err := json.Unmarshal(eiRaw, &eiStr) + if err == nil { logRecord.ExtraInfo = eiStr } delete(raw, "extra-info") @@ -148,7 +153,6 @@ func GetLogsDirectory() (string, error) { } func CreateDirectoryAndPath(dirPath string, filename string) error { - completeFilePath := dirPath + "/" + filename _, err := os.Stat(dirPath) @@ -170,5 +174,4 @@ func CreateDirectoryAndPath(dirPath string, filename string) error { } return nil - } diff --git a/internal/utils/liquidityManager.go b/internal/utils/liquidityManager.go index a39b0b90..b7f0873e 100644 --- a/internal/utils/liquidityManager.go +++ b/internal/utils/liquidityManager.go @@ -46,13 +46,11 @@ const LiquidityOut SwapType = "LiquidityOut" const LiquidityIn SwapType = "LiquidityIn" func (s SwapType) ToString() string { - switch s { case LiquidityOut: return "Out" case LiquidityIn: return "In" - } return "" } diff --git a/internal/utils/proofs.go b/internal/utils/proofs.go index 5eaa60b3..42b9c17b 100644 --- a/internal/utils/proofs.go +++ b/internal/utils/proofs.go @@ -37,6 +37,22 @@ func ParseErrorToCashuErrorCode(proofError error) (cashu.ErrorCode, *string) { message := cashu.ErrInvalidPreimage.Error() return cashu.PROOF_VERIFICATION_FAILED, &message + case errors.Is(proofError, cashu.ErrAmountlessInvoiceNotSupported): + message := cashu.ErrAmountlessInvoiceNotSupported.Error() + return cashu.AMOUNT_LESS_INVOICE_NOT_SUPPORTED, &message + + case errors.Is(proofError, cashu.ErrAmountOutsideLimit): + message := cashu.ErrAmountOutsideLimit.Error() + return cashu.INSUFICIENT_OUTSIDE_LIMIT, &message + + case errors.Is(proofError, cashu.ErrMintintDisabled): + message := cashu.ErrMintintDisabled.Error() + return cashu.MINTING_DISABLED, &message + + case errors.Is(proofError, cashu.ErrMintRequestAlreadyIssued): + message := cashu.ErrMintRequestAlreadyIssued.Error() + return cashu.QUOTE_ALREADY_ISSUED, &message + case errors.Is(proofError, cashu.ErrLocktimePassed): message := cashu.ErrLocktimePassed.Error() return cashu.UNKNOWN, &message @@ -49,6 +65,9 @@ func ParseErrorToCashuErrorCode(proofError error) (cashu.ErrorCode, *string) { case errors.Is(proofError, cashu.ErrProofSpent): message := cashu.ErrProofSpent.Error() return cashu.PROOF_ALREADY_SPENT, &message + case errors.Is(proofError, cashu.ErrProofPending): + message := cashu.ErrProofPending.Error() + return cashu.PROOFS_PENDING, &message case errors.Is(proofError, cashu.ErrNotSameUnits): message := cashu.ErrNotSameUnits.Error() @@ -66,6 +85,18 @@ func ParseErrorToCashuErrorCode(proofError error) (cashu.ErrorCode, *string) { message := cashu.ErrRepeatedInput.Error() return cashu.DUPLICATE_INPUTS, &message + case errors.Is(proofError, cashu.ErrAmountNotEqualToInvoice): + message := cashu.ErrAmountNotEqualToInvoice.Error() + return cashu.AMOUNT_NOT_EQUAL_TO_INVOICE, &message + + case errors.Is(proofError, cashu.ErrRequestNotPaid): + message := cashu.ErrRequestNotPaid.Error() + return cashu.REQUEST_NOT_PAID, &message + + case errors.Is(proofError, cashu.ErrQuoteIsPending): + message := cashu.ErrQuoteIsPending.Error() + return cashu.QUOTE_PENDING, &message + case errors.Is(proofError, cashu.ErrUnitNotSupported): message := cashu.ErrUnitNotSupported.Error() return cashu.UNIT_NOT_SUPPORTED, &message @@ -104,8 +135,8 @@ func ParseErrorToCashuErrorCode(proofError error) (cashu.ErrorCode, *string) { return cashu.UNKNOWN, nil } -// Sets some values being used by the mint like seen, secretY, seen, and pending state -func GetAndCalculateProofsValues(proofs *cashu.Proofs) (uint64, []cashu.WrappedPublicKey, error) { +// Sets the y and seen at field in by references and returns the Y's array +func GetAndCalculateProofsValues(proofs *cashu.Proofs) ([]cashu.WrappedPublicKey, error) { now := time.Now().Unix() var totalAmount uint64 secretsList := make([]cashu.WrappedPublicKey, len(*proofs)) @@ -115,14 +146,14 @@ func GetAndCalculateProofsValues(proofs *cashu.Proofs) (uint64, []cashu.WrappedP p, err := proof.HashSecretToCurve() if err != nil { - return 0, secretsList, fmt.Errorf("proof.HashSecretToCurve(). %w", err) + return nil, fmt.Errorf("proof.HashSecretToCurve(). %w", err) } secretsList[i] = p.Y (*proofs)[i] = p (*proofs)[i].SeenAt = now } - return totalAmount, secretsList, nil + return secretsList, nil } func GetMessagesForChange(overpaidFees uint64, outputs []cashu.BlindedMessage) []cashu.BlindedMessage { amounts := cashu.AmountSplit(overpaidFees) @@ -140,7 +171,6 @@ func GetMessagesForChange(overpaidFees uint64, outputs []cashu.BlindedMessage) [ for i := range outputs { outputs[i].Amount = amounts[i] } - } return outputs } diff --git a/internal/utils/proofs_test.go b/internal/utils/proofs_test.go index a6f15bd5..6f247d0b 100644 --- a/internal/utils/proofs_test.go +++ b/internal/utils/proofs_test.go @@ -16,13 +16,11 @@ func setListofEmptyBlindMessages(amounts int) []cashu.BlindedMessage { Amount: 0, } messages = append(messages, message) - } return messages } func TestGetChangeWithEnoughBlindMessages(t *testing.T) { - emptyBlindMessages := setListofEmptyBlindMessages(10) // create change for value of 2 @@ -30,7 +28,6 @@ func TestGetChangeWithEnoughBlindMessages(t *testing.T) { if len(change) != 1 { t.Errorf("Incorrect size for change slice %v, should be 1", len(change)) - } if change[0].Amount != 2 { @@ -43,11 +40,9 @@ func TestGetChangeWithEnoughBlindMessages(t *testing.T) { 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 @@ -60,11 +55,9 @@ func TestGetChangeWithOutEnoughBlindMessages(t *testing.T) { 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{ @@ -79,14 +72,12 @@ func MakeListofMockProofs(amounts int) []cashu.Proof { SeenAt: 0, } proofs = append(proofs, proof) - } return proofs } func TestGetValuesFromProofs(t *testing.T) { - listOfProofs := cashu.Proofs{ { C: cashu.WrappedPublicKey{PublicKey: nil}, @@ -112,20 +103,19 @@ func TestGetValuesFromProofs(t *testing.T) { }, } - TotalAmount, secretsList, err := GetAndCalculateProofsValues(&listOfProofs) + 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 listOfProofs.Amount() != 8 { + t.Errorf("Incorrect total amount %v. Should be 8", listOfProofs.Amount()) } if secretsList[0].ToHex() != "02aa4a2c024e41bd87e8c2758d5a7c2d81e09afe52f67fc8a69768bd73d515e28f" { - t.Errorf("Should be mock secret %v. Should be 8", TotalAmount) + t.Errorf("Should be mock secret %v", secretsList[0].ToHex()) } if listOfProofs[0].Y.ToHex() != "02aa4a2c024e41bd87e8c2758d5a7c2d81e09afe52f67fc8a69768bd73d515e28f" { t.Errorf("Incorrect Y: %v. ", listOfProofs[0].Y) } - } diff --git a/internal/utils/testing.go b/internal/utils/testing.go index 272854ff..af449709 100644 --- a/internal/utils/testing.go +++ b/internal/utils/testing.go @@ -101,7 +101,6 @@ func SetUpLightingNetworkTestEnviroment(ctx context.Context, names string) (test Started: true, }) if err != nil { - return nil, nil, nil, nil, fmt.Errorf("could not create Alice lnd container %w", err) } @@ -113,7 +112,7 @@ func SetUpLightingNetworkTestEnviroment(ctx context.Context, names string) (test buf := make([]byte, 1024) type LndAddress struct { - Address string + Address string `json:"address"` } var address LndAddress @@ -125,7 +124,6 @@ func SetUpLightingNetworkTestEnviroment(ctx context.Context, names string) (test if err != nil { log.Fatalln("json.Unmarshal: ", err) } - } if err != nil { break @@ -474,7 +472,6 @@ func SetUpLightingNetworkTestEnviroment(ctx context.Context, names string) (test // return alice, bob, btcNode, aliceLnbits, error return lndAliceC, LndBobC, btcdC, aliceLnbitsC, nil - } func ExtractInternalFile(ctx context.Context, container testcontainers.Container, path string) (string, error) { catData, err := container.CopyFileFromContainer(ctx, path) diff --git a/pkg/crypto/bdhke.go b/pkg/crypto/bdhke.go index 30da8e5f..61ec002d 100644 --- a/pkg/crypto/bdhke.go +++ b/pkg/crypto/bdhke.go @@ -51,7 +51,6 @@ func HashToCurve(message []byte) (*secp256k1.PublicKey, error) { // B_ = Y + rG func BlindMessage(secret string, r *secp256k1.PrivateKey) (*secp256k1.PublicKey, *secp256k1.PrivateKey, error) { - var ypoint, rpoint, blindedMessage secp256k1.JacobianPoint Y, err := HashToCurve([]byte(secret)) if err != nil { @@ -85,7 +84,6 @@ func SignBlindedMessage(B_ *secp256k1.PublicKey, k *secp256k1.PrivateKey) *secp2 // C = C_ - rK func UnblindSignature(C_ *secp256k1.PublicKey, r *secp256k1.PrivateKey, K *secp256k1.PublicKey) *secp256k1.PublicKey { - var Kpoint, rKPoint, CPoint secp256k1.JacobianPoint K.AsJacobian(&Kpoint) diff --git a/pkg/crypto/bdhke_test.go b/pkg/crypto/bdhke_test.go index 29888a29..d9fa67f8 100644 --- a/pkg/crypto/bdhke_test.go +++ b/pkg/crypto/bdhke_test.go @@ -145,7 +145,6 @@ func TestUnblindSignature(t *testing.T) { if CHex != test.expected { t.Errorf("expected '%v' but got '%v' instead\n", test.expected, CHex) } - } } @@ -213,5 +212,4 @@ func TestHashE(t *testing.T) { if hex.EncodeToString(hash[:]) != "a4dc034b74338c28c6bc3ea49731f2a24440fc7c4affc08b31a93fc9fbe6401e" { t.Errorf("hash is not correct. got: \n\n %v", hex.EncodeToString(hash[:])) } - } diff --git a/test/configTest/setup.go b/test/configTest/setup.go index ebeb4fd3..3b79b952 100644 --- a/test/configTest/setup.go +++ b/test/configTest/setup.go @@ -12,15 +12,12 @@ type ConfigFiles struct { } func CopyConfigFiles(filepath string) (ConfigFiles, error) { - var config ConfigFiles file, err := os.ReadFile(filepath) if err != nil { - return config, fmt.Errorf("could not read file: %w", err) - } config.TomlFile = file @@ -29,7 +26,6 @@ func CopyConfigFiles(filepath string) (ConfigFiles, error) { } func RemoveConfigFile(filepath string) error { - err := os.Remove(filepath) if err != nil { return fmt.Errorf("os.Remove(), %w", err) diff --git a/test/setupTest/lnd_test.go b/test/setupTest/lnd_test.go index 859a6191..b0e160a3 100644 --- a/test/setupTest/lnd_test.go +++ b/test/setupTest/lnd_test.go @@ -33,8 +33,7 @@ func TestSetupLightingCommsLND(t *testing.T) { t.Fatalf("setUpLightingNetworkEnviroment %+v", err) } - //nolint:exhaustruct - invoice, err := lndWallet.RequestInvoice(cashu.MintRequestDB{}, cashu.Amount{Amount: 1000, Unit: cashu.Sat}) + invoice, err := lndWallet.RequestInvoice(cashu.Amount{Amount: 1000, Unit: cashu.Sat}, nil) if err != nil { t.Fatalf("could not setup lighting comms %+v", err) } @@ -42,7 +41,6 @@ func TestSetupLightingCommsLND(t *testing.T) { if len(invoice.PaymentRequest) == 0 { t.Fatalf("There is no payment request %+v", err) } - } func TestSetupLightingCommsLnBits(t *testing.T) { @@ -63,8 +61,7 @@ func TestSetupLightingCommsLnBits(t *testing.T) { if err != nil { t.Fatalf("setUpLightingNetworkEnviroment %+v", err) } - //nolint:exhaustruct - invoice, err := lnbitsWallet.RequestInvoice(cashu.MintRequestDB{}, cashu.Amount{Amount: 1000, Unit: cashu.Sat}) + invoice, err := lnbitsWallet.RequestInvoice(cashu.Amount{Amount: 1000, Unit: cashu.Sat}, nil) if err != nil { t.Fatalf("could not setup lighting comms %+v", err) } @@ -72,5 +69,4 @@ func TestSetupLightingCommsLnBits(t *testing.T) { if len(invoice.PaymentRequest) == 0 { t.Fatalf("There is no payment request %+v", err) } - } From f677cab6e4675ac2ff668ef695796bec367ce0a3 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Wed, 15 Apr 2026 10:23:06 +0200 Subject: [PATCH 2/8] renaming of function names --- cmd/nutmix/main.go | 2 +- cmd/nutmix/payment_error_handling_test.go | 14 ++-- internal/mint/melting.go | 67 ++++++++++--------- internal/mint/mint_test.go | 12 ++-- internal/mint/minting.go | 42 ++++++------ internal/mint/swapping.go | 38 +++++------ internal/mint/transaction_persistence_test.go | 24 +++---- internal/mint/utils_test.go | 2 +- internal/routes/auth.go | 2 +- internal/routes/bolt11.go | 24 +++---- internal/routes/mint.go | 6 +- internal/routes/routes.go | 4 +- 12 files changed, 119 insertions(+), 118 deletions(-) diff --git a/cmd/nutmix/main.go b/cmd/nutmix/main.go index e9eecec6..7c1009dd 100644 --- a/cmd/nutmix/main.go +++ b/cmd/nutmix/main.go @@ -134,7 +134,7 @@ func main() { // Add per-request timeout middleware (sets context deadline for handlers) r.Use(middleware.TimeoutMiddleware(90 * time.Second)) - err = mint.CheckPendingQuoteAndProofs() + err = mint.ReconcilePendingMeltQuotes() if err != nil { slog.Error("SetUpMint", slog.Any("error", err)) return diff --git a/cmd/nutmix/payment_error_handling_test.go b/cmd/nutmix/payment_error_handling_test.go index 87cab8eb..ffefd3e7 100644 --- a/cmd/nutmix/payment_error_handling_test.go +++ b/cmd/nutmix/payment_error_handling_test.go @@ -263,9 +263,9 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { Signature: nil, } - postMintResponse, err := mintInstance.Mint(ctx, mintRequest, mint.Bolt11) + postMintResponse, err := mintInstance.IssueTokens(ctx, mintRequest, mint.Bolt11) if err != nil { - t.Fatalf("mintInstance.Mint(ctx, mintRequest, mint.Bolt11): %v", err) + t.Fatalf("mintInstance.IssueTokens(ctx, mintRequest, mint.Bolt11): %v", err) } /// start doing melt quote @@ -274,9 +274,9 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { Request: RegtestRequest, Options: cashu.PostMeltQuoteBolt11Options{Mpp: nil}, } - postMeltQuoteResponse, err := mintInstance.MeltQuote(ctx, meltQuoteRequest, mint.Bolt11) + postMeltQuoteResponse, err := mintInstance.CreateMeltQuote(ctx, meltQuoteRequest, mint.Bolt11) if err != nil { - t.Fatalf("mintInstance.MeltQuote(ctx, meltQuoteRequest, mint.Bolt11): %v", err) + t.Fatalf("mintInstance.CreateMeltQuote(ctx, meltQuoteRequest, mint.Bolt11): %v", err) } // try melting @@ -303,9 +303,9 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { Outputs: nil, } - postMeltResponse, err := mintInstance.Melt(ctx, meltRequest, mint.Bolt11) + postMeltResponse, err := mintInstance.ExecuteMelt(ctx, meltRequest, mint.Bolt11) if err != nil { - t.Fatalf("mintInstance.Melt(ctx, meltRequest, mint.Bolt11): %v", err) + t.Fatalf("mintInstance.ExecuteMelt(ctx, meltRequest, mint.Bolt11): %v", err) } if postMeltResponse.State == cashu.PAID { @@ -333,7 +333,7 @@ func TestPaymentFailureButPendingCheckPaymentPostgresFakeWallet(t *testing.T) { return } - _, err = mintInstance.Melt(ctx, meltRequest, mint.Bolt11) + _, err = mintInstance.ExecuteMelt(ctx, meltRequest, mint.Bolt11) if !errors.Is(err, cashu.ErrQuoteIsPending) { t.Fatalf("Expected ErrQuoteIsPending, got %v", err) } diff --git a/internal/mint/melting.go b/internal/mint/melting.go index 0ef51feb..faf19599 100644 --- a/internal/mint/melting.go +++ b/internal/mint/melting.go @@ -15,12 +15,12 @@ import ( "github.com/lightningnetwork/lnd/zpay32" ) -func (m *Mint) MeltQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request, method METHOD) (cashu.MeltRequestDB, error) { +func (m *Mint) CreateMeltQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request, method METHOD) (cashu.MeltRequestDB, error) { switch method { case Bolt11: - response, err := m.bolt11CreateMelt(ctx, meltRequest) + response, err := m.createBolt11MeltQuote(ctx, meltRequest) if err != nil { - return cashu.MeltRequestDB{}, fmt.Errorf("m.bolt11CreateMelt(ctx, meltRequest). %w ", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.createBolt11MeltQuote(ctx, meltRequest). %w ", err) } return response, nil @@ -29,19 +29,20 @@ func (m *Mint) MeltQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBol } } -func (m *Mint) bolt11CreateMelt(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request) (cashu.MeltRequestDB, error) { - requestData, err := m.bolt11ValidateMeltRequestQuote(ctx, meltRequest) +func (m *Mint) createBolt11MeltQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request) (cashu.MeltRequestDB, error) { + requestData, err := m.validateBolt11MeltQuoteRequest(ctx, meltRequest) if err != nil { - return cashu.MeltRequestDB{}, fmt.Errorf("m.bolt11ValidateMeltRequestQuote. %w ", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.validateBolt11MeltQuoteRequest. %w ", err) } - dbRequest, err := m.bolt11CreateMeltRequest(ctx, meltRequest, requestData) + dbRequest, err := m.createAndStoreBolt11MeltQuote(ctx, meltRequest, requestData) if err != nil { - return cashu.MeltRequestDB{}, fmt.Errorf("m.bolt11CreateMeltRequest. %w ", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.createAndStoreBolt11MeltQuote. %w ", err) } return dbRequest, nil } -func (m *Mint) bolt11CreateMeltRequest(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request, requestData bolt11MeltReqData) (cashu.MeltRequestDB, error) { + +func (m *Mint) createAndStoreBolt11MeltQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request, requestData bolt11MeltReqData) (cashu.MeltRequestDB, error) { quoteId, err := utils.RandomHash() if err != nil { return cashu.MeltRequestDB{}, fmt.Errorf("utils.RandomHash(). %w", err) @@ -112,7 +113,7 @@ type bolt11MeltReqData struct { Mpp bool } -func (m *Mint) bolt11ValidateMeltRequestQuote(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request) (bolt11MeltReqData, error) { +func (m *Mint) validateBolt11MeltQuoteRequest(ctx context.Context, meltRequest cashu.PostMeltQuoteBolt11Request) (bolt11MeltReqData, error) { unit, err := cashu.UnitFromString(meltRequest.Unit) if err != nil { return bolt11MeltReqData{}, errors.Join(err, cashu.ErrUnitNotSupported) @@ -212,7 +213,7 @@ func (m *Mint) settleIfInternalMelt(tx pgx.Tx, meltQuote cashu.MeltRequestDB) (c return meltQuote, nil } -func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.MeltRequestDB, error) { +func (m *Mint) RefreshMeltQuoteState(ctx context.Context, quoteId string) (cashu.MeltRequestDB, error) { initialTx, err := m.MintDB.GetTx(ctx) if err != nil { return cashu.MeltRequestDB{}, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) @@ -390,7 +391,7 @@ func (m *Mint) CheckMeltQuoteState(ctx context.Context, quoteId string) (cashu.M return quote, nil } -func (m *Mint) CheckPendingQuoteAndProofs() error { +func (m *Mint) ReconcilePendingMeltQuotes() error { quotes, err := m.MintDB.GetMeltQuotesByState(cashu.PENDING) if err != nil { return fmt.Errorf("m.MintDB.GetMeltQuotesByState(cashu.PENDING). %w", err) @@ -398,9 +399,9 @@ func (m *Mint) CheckPendingQuoteAndProofs() error { for _, quote := range quotes { slog.Info("Attempting to solve pending quote for", slog.Any("quote", quote)) - quote, err := m.CheckMeltQuoteState(context.Background(), quote.Quote) + quote, err := m.RefreshMeltQuoteState(context.Background(), quote.Quote) if err != nil { - return fmt.Errorf("m.CheckMeltQuoteState(ctx, quote.Quote). %w", err) + return fmt.Errorf("m.RefreshMeltQuoteState(ctx, quote.Quote). %w", err) } slog.Info("Melt quote state", slog.String("quote", quote.Quote), slog.String("state", string(quote.State))) @@ -415,7 +416,7 @@ type bolt11MeltData struct { Unit cashu.Unit } -func (m *Mint) bolt11MeltValidate(meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB) (bolt11MeltData, error) { +func (m *Mint) validateBolt11MeltInputs(meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB) (bolt11MeltData, error) { if len(meltRequest.Inputs) == 0 { return bolt11MeltData{}, fmt.Errorf("inputs or outputs are empty") } @@ -500,7 +501,7 @@ func (m *Mint) bolt11MeltValidate(meltRequest cashu.PostMeltBolt11Request, quote return bolt11MeltData{Fee: cashu.NewAmount(proofUnit, uint64(fee)), Unit: unit, AmountProofs: cashu.NewAmount(proofUnit, proofsAmount)}, nil } -func (m *Mint) validateMeltStatusAndSpent(ctx context.Context, meltRequest cashu.PostMeltBolt11Request) (cashu.MeltRequestDB, error) { +func (m *Mint) reserveMeltInputsAndMarkPending(ctx context.Context, meltRequest cashu.PostMeltBolt11Request) (cashu.MeltRequestDB, error) { // check if proofs are spent and if outputs are spent sizeCheckTx, err := m.MintDB.GetTx(ctx) if err != nil { @@ -528,14 +529,14 @@ func (m *Mint) validateMeltStatusAndSpent(ctx context.Context, meltRequest cashu return cashu.MeltRequestDB{}, cashu.ErrMeltAlreadyPaid } - proofs, err := m.checkProofSpent(sizeCheckTx, meltRequest.Inputs) + proofs, err := m.validateProofsUnspent(sizeCheckTx, meltRequest.Inputs) if err != nil { - return cashu.MeltRequestDB{}, fmt.Errorf("m.checkProofSpent(sizeCheckTx, request.Inputs). %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.validateProofsUnspent(sizeCheckTx, request.Inputs). %w", err) } - err = m.CheckOutputSpent(sizeCheckTx, meltRequest.Outputs) + err = m.ValidateOutputsNotSpent(sizeCheckTx, meltRequest.Outputs) if err != nil { - return cashu.MeltRequestDB{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + return cashu.MeltRequestDB{}, fmt.Errorf("m.ValidateOutputsNotSpent(sizeCheckTx, request.Outputs). %w", err) } proofs.SetPendingAndQuoteRef(quote.Quote) @@ -567,7 +568,7 @@ func (m *Mint) validateMeltStatusAndSpent(ctx context.Context, meltRequest cashu return quote, nil } -func (m *Mint) bolt11PayInvoice(ctx context.Context, meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB) (cashu.MeltRequestDB, cashu.Amount, error) { +func (m *Mint) attemptBolt11MeltPayment(ctx context.Context, meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB) (cashu.MeltRequestDB, cashu.Amount, error) { // Commit all blind messages and proofs as pending before going over the network invoice, err := zpay32.Decode(quote.Request, m.LightningBackend.GetNetwork()) if err != nil { @@ -691,7 +692,7 @@ func (m *Mint) bolt11PayInvoice(ctx context.Context, meltRequest cashu.PostMeltB return quote, cashu.Amount{Amount: 0, Unit: unit}, nil } -func (m *Mint) bolt11MeltBurnTokens(ctx context.Context, meltData bolt11MeltData, meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB, paidLightningFeeSat cashu.Amount) (cashu.MeltRequestDB, cashu.PostMeltQuoteBolt11Response, cashu.Proofs, error) { +func (m *Mint) finalizePaidBolt11Melt(ctx context.Context, meltData bolt11MeltData, meltRequest cashu.PostMeltBolt11Request, quote cashu.MeltRequestDB, paidLightningFeeSat cashu.Amount) (cashu.MeltRequestDB, cashu.PostMeltQuoteBolt11Response, cashu.Proofs, error) { err := paidLightningFeeSat.To(meltData.Unit) if err != nil { return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, nil, fmt.Errorf("paidLightningFeeSat.To(meltData) %w", err) @@ -768,29 +769,29 @@ func (m *Mint) bolt11MeltBurnTokens(ctx context.Context, meltData bolt11MeltData return quote, response, meltRequest.Inputs, nil } func (m *Mint) bolt11Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request) (cashu.MeltRequestDB, cashu.PostMeltQuoteBolt11Response, error) { - quote, err := m.CheckMeltQuoteState(ctx, meltRequest.Quote) + quote, err := m.RefreshMeltQuoteState(ctx, meltRequest.Quote) if err != nil { - return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("mint.CheckMeltQuoteState(ctx, quoteId): %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.RefreshMeltQuoteState(ctx, quoteId): %w", err) } - meltRequestData, err := m.bolt11MeltValidate(meltRequest, quote) + meltRequestData, err := m.validateBolt11MeltInputs(meltRequest, quote) if err != nil { - return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.bolt11MeltValidate(meltRequest, quote): %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.validateBolt11MeltInputs(meltRequest, quote): %w", err) } - quote, err = m.validateMeltStatusAndSpent(ctx, meltRequest) + quote, err = m.reserveMeltInputsAndMarkPending(ctx, meltRequest) if err != nil { - return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.validateMeltStatusAndSpent(ctx, meltRequest): %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.reserveMeltInputsAndMarkPending(ctx, meltRequest): %w", err) } - quote, lnFee, err := m.bolt11PayInvoice(ctx, meltRequest, quote) + quote, lnFee, err := m.attemptBolt11MeltPayment(ctx, meltRequest, quote) if err != nil { - return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.bolt11PayInvoice(ctx, meltRequest, quote): %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.attemptBolt11MeltPayment(ctx, meltRequest, quote): %w", err) } if quote.State == cashu.PAID { - quote, response, spentProofs, err := m.bolt11MeltBurnTokens(ctx, meltRequestData, meltRequest, quote, lnFee) + quote, response, spentProofs, err := m.finalizePaidBolt11Melt(ctx, meltRequestData, meltRequest, quote, lnFee) if err != nil { - return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.bolt11MeltBurnTokens(ctx, meltRequestData, meltRequest, quote, lnFee): %w", err) + return cashu.MeltRequestDB{}, cashu.PostMeltQuoteBolt11Response{}, fmt.Errorf("m.finalizePaidBolt11Melt(ctx, meltRequestData, meltRequest, quote, lnFee): %w", err) } go m.Observer.SendProofsEvent(spentProofs) @@ -804,7 +805,7 @@ func (m *Mint) bolt11Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11R return quote, quote.GetPostMeltQuoteResponse(), nil } -func (m *Mint) Melt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request, method METHOD) (cashu.PostMeltQuoteBolt11Response, error) { +func (m *Mint) ExecuteMelt(ctx context.Context, meltRequest cashu.PostMeltBolt11Request, method METHOD) (cashu.PostMeltQuoteBolt11Response, error) { switch method { case Bolt11: _, response, err := m.bolt11Melt(ctx, meltRequest) diff --git a/internal/mint/mint_test.go b/internal/mint/mint_test.go index aeba1d92..b383c023 100644 --- a/internal/mint/mint_test.go +++ b/internal/mint/mint_test.go @@ -182,9 +182,9 @@ func TestPendingQuotesAndProofsWithPostgresAndMockLNSuccess(t *testing.T) { t.Fatalf("SetupDataOnDB(mint): %+v ", err) } - meltRequest, err := mint.CheckMeltQuoteState(context.Background(), quoteId) + meltRequest, err := mint.RefreshMeltQuoteState(context.Background(), quoteId) if err != nil { - t.Fatalf("mint.CheckMeltQuoteState(quoteId): %+v ", err) + t.Fatalf("mint.RefreshMeltQuoteState(quoteId): %+v ", err) } if meltRequest.Quote != quoteId { @@ -268,9 +268,9 @@ func TestPendingQuotesAndProofsWithPostgresAndMockLNFail(t *testing.T) { t.Fatalf("SetupDataOnDB(mint): %+v ", err) } - meltRequest, err := mint.CheckMeltQuoteState(context.Background(), quoteId) + meltRequest, err := mint.RefreshMeltQuoteState(context.Background(), quoteId) if err != nil { - t.Fatalf("mint.CheckMeltQuoteState(quoteId): %+v ", err) + t.Fatalf("mint.RefreshMeltQuoteState(quoteId): %+v ", err) } if meltRequest.Quote != quoteId { @@ -341,9 +341,9 @@ func TestPendingQuotesAndProofsWithPostgresAndMockLNPending(t *testing.T) { t.Fatalf("SetupDataOnDB(mint): %+v ", err) } - meltRequest, err := mint.CheckMeltQuoteState(context.Background(), quoteId) + meltRequest, err := mint.RefreshMeltQuoteState(context.Background(), quoteId) if err != nil { - t.Fatalf("mint.CheckMeltQuoteState(quoteId): %+v ", err) + t.Fatalf("mint.RefreshMeltQuoteState(quoteId): %+v ", err) } if meltRequest.Quote != quoteId { diff --git a/internal/mint/minting.go b/internal/mint/minting.go index 6d879c7d..455d07d7 100644 --- a/internal/mint/minting.go +++ b/internal/mint/minting.go @@ -25,9 +25,9 @@ func (m *Mint) CreateMintQuote(ctx context.Context, request cashu.PostMintQuoteB if !supported { return cashu.PostMintQuoteBolt11Response{}, errors.Join(err, cashu.ErrUnitNotSupported) } - response, err := m.bolt11GenerateMintQuote(ctx, request, unit) + response, err := m.createBolt11MintQuote(ctx, request, unit) if err != nil { - return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf("m.generateBolt11MintRequest(request,unit). %w", err) + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf("m.createBolt11MintQuote(ctx, request, unit). %w", err) } return response, nil @@ -60,7 +60,7 @@ func (m *Mint) validateMintConfiguration(request cashu.PostMintQuoteBolt11Reques return unit, nil } -func (m *Mint) bolt11GenerateMintQuote(ctx context.Context, request cashu.PostMintQuoteBolt11Request, unit cashu.Unit) (cashu.PostMintQuoteBolt11Response, error) { +func (m *Mint) createBolt11MintQuote(ctx context.Context, request cashu.PostMintQuoteBolt11Request, unit cashu.Unit) (cashu.PostMintQuoteBolt11Response, error) { resInvoice, err := m.LightningBackend.RequestInvoice(cashu.NewAmount(unit, request.Amount), request.Description) if err != nil { return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.LightningBackend.RequestInvoice. %w", err) @@ -113,7 +113,7 @@ func (m *Mint) bolt11GenerateMintQuote(ctx context.Context, request cashu.PostMi } // FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration -func (m *Mint) MintQuoteStatus(ctx context.Context, quoteId string, method METHOD) (cashu.PostMintQuoteBolt11Response, error) { +func (m *Mint) RefreshMintQuoteStatus(ctx context.Context, quoteId string, method METHOD) (cashu.PostMintQuoteBolt11Response, error) { tx, err := m.MintDB.GetTx(ctx) if err != nil { return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf(" m.MintDB.GetTx(ctx). %w", err) @@ -139,9 +139,9 @@ func (m *Mint) MintQuoteStatus(ctx context.Context, quoteId string, method METHO if quote.State == cashu.PAID || quote.State == cashu.ISSUED { return quote.PostMintQuoteBolt11Response(), nil } - bolt11Quote, err := m.bolt11CheckQuote(ctx, quote, method) + bolt11Quote, err := m.reconcileBolt11MintQuoteState(ctx, quote, method) if err != nil { - return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf("m.bolt11CheckQuote(ctx, quote, method). %w", err) + return cashu.PostMintQuoteBolt11Response{}, fmt.Errorf("m.reconcileBolt11MintQuoteState(ctx, quote, method). %w", err) } return bolt11Quote.PostMintQuoteBolt11Response(), nil @@ -151,7 +151,7 @@ func (m *Mint) MintQuoteStatus(ctx context.Context, quoteId string, method METHO } // FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration -func (m *Mint) bolt11CheckQuote(ctx context.Context, request cashu.MintRequestDB, method METHOD) (cashu.MintRequestDB, error) { +func (m *Mint) reconcileBolt11MintQuoteState(ctx context.Context, request cashu.MintRequestDB, method METHOD) (cashu.MintRequestDB, error) { if method != Bolt11 { return cashu.MintRequestDB{}, fmt.Errorf("request method is not BOLT11") } @@ -162,7 +162,7 @@ func (m *Mint) bolt11CheckQuote(ctx context.Context, request cashu.MintRequestDB status, _, err := m.LightningBackend.CheckReceived(request, invoice) if err != nil { - return cashu.MintRequestDB{}, fmt.Errorf("mint.VerifyLightingPaymentHappened(pool). %w", err) + return cashu.MintRequestDB{}, fmt.Errorf("m.LightningBackend.CheckReceived(request, invoice). %w", err) } stateChangeTX, err := m.MintDB.GetTx(ctx) if err != nil { @@ -204,10 +204,10 @@ func (m *Mint) bolt11CheckQuote(ctx context.Context, request cashu.MintRequestDB return quote, nil } -func (m *Mint) Mint(ctx context.Context, request cashu.PostMintBolt11Request, method METHOD) (cashu.PostMintBolt11Response, error) { - mintReq, err := m.mintRequestValidate(ctx, request) +func (m *Mint) IssueTokens(ctx context.Context, request cashu.PostMintBolt11Request, method METHOD) (cashu.PostMintBolt11Response, error) { + mintReq, err := m.loadAndValidateMintQuoteForIssuance(ctx, request) if err != nil { - return cashu.PostMintBolt11Response{}, fmt.Errorf(" mintRequestValidate(ctx, request). %w", err) + return cashu.PostMintBolt11Response{}, fmt.Errorf("m.loadAndValidateMintQuoteForIssuance(ctx, request). %w", err) } switch method { case Bolt11: @@ -223,7 +223,7 @@ func (m *Mint) Mint(ctx context.Context, request cashu.PostMintBolt11Request, me } // takes the general values of the minting process and analyses them even before going to the method branching. -func (m *Mint) mintRequestValidate(ctx context.Context, request cashu.PostMintBolt11Request) (cashu.MintRequestDB, error) { +func (m *Mint) loadAndValidateMintQuoteForIssuance(ctx context.Context, request cashu.PostMintBolt11Request) (cashu.MintRequestDB, error) { preparationTx, err := m.MintDB.GetTx(ctx) if err != nil { return cashu.MintRequestDB{}, fmt.Errorf(" m.MintDB.GetTx(ctx). %w", err) @@ -244,9 +244,9 @@ func (m *Mint) mintRequestValidate(ctx context.Context, request cashu.PostMintBo if err != nil { return cashu.MintRequestDB{}, fmt.Errorf(" m.MintDB.Commit(ctx, tx). %w", err) } - err = m.validateMintStatusAndAuth(request, quote) + err = m.validateMintIssuanceAuth(request, quote) if err != nil { - return cashu.MintRequestDB{}, fmt.Errorf(" m.bolt11ValidateMint(ctx, request, quote). %w", err) + return cashu.MintRequestDB{}, fmt.Errorf("m.validateMintIssuanceAuth(request, quote). %w", err) } keysets, err := m.Signer.GetKeysets() if err != nil { @@ -275,9 +275,9 @@ func (m *Mint) mintRequestValidate(ctx context.Context, request cashu.PostMintBo } } }() - err = m.CheckOutputSpent(sizeCheckTx, request.Outputs) + err = m.ValidateOutputsNotSpent(sizeCheckTx, request.Outputs) if err != nil { - return cashu.MintRequestDB{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + return cashu.MintRequestDB{}, fmt.Errorf("m.ValidateOutputsNotSpent(sizeCheckTx, request.Outputs). %w", err) } err = m.MintDB.Commit(ctx, sizeCheckTx) if err != nil { @@ -319,9 +319,9 @@ func (m *Mint) bolt11Mint(ctx context.Context, request cashu.PostMintBolt11Reque } if mintReq.State != cashu.PAID { - mintReq, err = m.bolt11CheckQuote(ctx, mintReq, method) + mintReq, err = m.reconcileBolt11MintQuoteState(ctx, mintReq, method) if err != nil { - return cashu.PostMintBolt11Response{}, fmt.Errorf("m.bolt11CheckQuote(ctx, quote, method). %w", err) + return cashu.PostMintBolt11Response{}, fmt.Errorf("m.reconcileBolt11MintQuoteState(ctx, quote, method). %w", err) } } @@ -329,14 +329,14 @@ func (m *Mint) bolt11Mint(ctx context.Context, request cashu.PostMintBolt11Reque return cashu.PostMintBolt11Response{}, cashu.ErrRequestNotPaid } - blindSigs, err := m.signAndSaveSigs(ctx, request, mintReq) + blindSigs, err := m.signMintOutputsAndMarkIssued(ctx, request, mintReq) if err != nil { return cashu.PostMintBolt11Response{}, err } return cashu.PostMintBolt11Response{Signatures: blindSigs}, nil } -func (m *Mint) signAndSaveSigs(ctx context.Context, request cashu.PostMintBolt11Request, mintRequestDB cashu.MintRequestDB) ([]cashu.BlindSignature, error) { +func (m *Mint) signMintOutputsAndMarkIssued(ctx context.Context, request cashu.PostMintBolt11Request, mintRequestDB cashu.MintRequestDB) ([]cashu.BlindSignature, error) { blindedSignatures, recoverySigsDb, err := m.Signer.SignBlindMessages(request.Outputs) if err != nil { return nil, fmt.Errorf("m.Signer.SignBlindMessages(request.Outputs) %w", err) @@ -373,7 +373,7 @@ func (m *Mint) signAndSaveSigs(ctx context.Context, request cashu.PostMintBolt11 return blindedSignatures, nil } -func (m *Mint) validateMintStatusAndAuth(request cashu.PostMintBolt11Request, mintRequestDB cashu.MintRequestDB) error { +func (m *Mint) validateMintIssuanceAuth(request cashu.PostMintBolt11Request, mintRequestDB cashu.MintRequestDB) error { if mintRequestDB.Minted { return cashu.ErrMintRequestAlreadyIssued } diff --git a/internal/mint/swapping.go b/internal/mint/swapping.go index 8c77621c..450bd163 100644 --- a/internal/mint/swapping.go +++ b/internal/mint/swapping.go @@ -11,16 +11,16 @@ import ( "github.com/lescuer97/nutmix/internal/utils" ) -func (m *Mint) Swap(ctx context.Context, request cashu.PostSwapRequest) (cashu.PostSwapResponse, error) { - amountValidationErr := m.swapRequestValidateAmount(request) +func (m *Mint) ExecuteSwap(ctx context.Context, request cashu.PostSwapRequest) (cashu.PostSwapResponse, error) { + amountValidationErr := m.validateSwapBalanceAndUnits(request) if amountValidationErr != nil { - return cashu.PostSwapResponse{}, fmt.Errorf("m.validateInputAndOutput(request.Inputs, request.Outputs). %w", amountValidationErr) + return cashu.PostSwapResponse{}, fmt.Errorf("m.validateSwapBalanceAndUnits(request). %w", amountValidationErr) } // validate sig all - err := m.validateSwapRequest(request) + err := m.validateSwapProofsAndSpendConditions(request) if err != nil { - return cashu.PostSwapResponse{}, fmt.Errorf("m.validateInputAndOutput(request.Inputs, request.Outputs). %w", err) + return cashu.PostSwapResponse{}, fmt.Errorf("m.validateSwapProofsAndSpendConditions(request). %w", err) } // check if proofs are spent and if outputs are spent @@ -37,30 +37,30 @@ func (m *Mint) Swap(ctx context.Context, request cashu.PostSwapRequest) (cashu.P } }() - proofs, err := m.checkProofSpent(sizeCheckTx, request.Inputs) + proofs, err := m.validateProofsUnspent(sizeCheckTx, request.Inputs) if err != nil { - return cashu.PostSwapResponse{}, fmt.Errorf("m.checkProofSpent(sizeCheckTx, request.Inputs). %w", err) + return cashu.PostSwapResponse{}, fmt.Errorf("m.validateProofsUnspent(sizeCheckTx, request.Inputs). %w", err) } - err = m.CheckOutputSpent(sizeCheckTx, request.Outputs) + err = m.ValidateOutputsNotSpent(sizeCheckTx, request.Outputs) if err != nil { - return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + return cashu.PostSwapResponse{}, fmt.Errorf("m.ValidateOutputsNotSpent(sizeCheckTx, request.Outputs). %w", err) } proofs.SetProofsState(cashu.PROOF_PENDING) err = m.MintDB.SaveProof(sizeCheckTx, proofs) if err != nil { - return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + return cashu.PostSwapResponse{}, fmt.Errorf("m.MintDB.SaveProof(sizeCheckTx, proofs). %w", err) } err = sizeCheckTx.Commit(ctx) if err != nil { - return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + return cashu.PostSwapResponse{}, fmt.Errorf("sizeCheckTx.Commit(ctx). %w", err) } - blindSignatures, err := m.signAndSetInputs(ctx, proofs, request) + blindSignatures, err := m.signSwapOutputsAndMarkInputsSpent(ctx, proofs, request) if err != nil { - return cashu.PostSwapResponse{}, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + return cashu.PostSwapResponse{}, fmt.Errorf("m.signSwapOutputsAndMarkInputsSpent(ctx, proofs, request). %w", err) } // mark as pending and sign @@ -69,7 +69,7 @@ func (m *Mint) Swap(ctx context.Context, request cashu.PostSwapRequest) (cashu.P }, nil } -func (m *Mint) signAndSetInputs(ctx context.Context, inputs cashu.Proofs, swapRequest cashu.PostSwapRequest) ([]cashu.BlindSignature, error) { +func (m *Mint) signSwapOutputsAndMarkInputsSpent(ctx context.Context, inputs cashu.Proofs, swapRequest cashu.PostSwapRequest) ([]cashu.BlindSignature, error) { // sign the outputs blindedSignatures, recoverySigsDb, err := m.Signer.SignBlindMessages(swapRequest.Outputs) if err != nil { @@ -78,7 +78,7 @@ func (m *Mint) signAndSetInputs(ctx context.Context, inputs cashu.Proofs, swapRe afterSigningTx, err := m.MintDB.GetTx(ctx) if err != nil { - return nil, fmt.Errorf("m.checkOutputSpent(sizeCheckTx, request.Outputs). %w", err) + return nil, fmt.Errorf("m.MintDB.GetTx(ctx). %w", err) } defer func() { rollbackErr := m.MintDB.Rollback(ctx, afterSigningTx) @@ -103,7 +103,7 @@ func (m *Mint) signAndSetInputs(ctx context.Context, inputs cashu.Proofs, swapRe return blindedSignatures, nil } -func (m *Mint) validateSwapRequest(request cashu.PostSwapRequest) error { +func (m *Mint) validateSwapProofsAndSpendConditions(request cashu.PostSwapRequest) error { // validate if the proofs are correctly signed err := m.VerifyProofsBDHKE(request.Inputs) if err != nil { @@ -132,7 +132,7 @@ func (m *Mint) validateSwapRequest(request cashu.PostSwapRequest) error { return nil } -func (m *Mint) swapRequestValidateAmount(request cashu.PostSwapRequest) error { +func (m *Mint) validateSwapBalanceAndUnits(request cashu.PostSwapRequest) error { if len(request.Inputs) == 0 || len(request.Outputs) == 0 { return fmt.Errorf("inputs or outputs are empty") } @@ -175,7 +175,7 @@ func (m *Mint) swapRequestValidateAmount(request cashu.PostSwapRequest) error { } // returns the proofs with the Y's and seen at. -func (m *Mint) checkProofSpent(tx pgx.Tx, proofs cashu.Proofs) (cashu.Proofs, error) { +func (m *Mint) validateProofsUnspent(tx pgx.Tx, proofs cashu.Proofs) (cashu.Proofs, error) { // gets the list of Y's. You need to calculate YsList, err := utils.GetAndCalculateProofsValues(&proofs) if err != nil { @@ -199,7 +199,7 @@ func (m *Mint) checkProofSpent(tx pgx.Tx, proofs cashu.Proofs) (cashu.Proofs, er return proofs, nil } -func (m *Mint) CheckOutputSpent(tx pgx.Tx, blindedMessages cashu.BlindedMessages) error { +func (m *Mint) ValidateOutputsNotSpent(tx pgx.Tx, blindedMessages cashu.BlindedMessages) error { outputsMap := make(map[string]bool) blindingFactors := []cashu.WrappedPublicKey{} diff --git a/internal/mint/transaction_persistence_test.go b/internal/mint/transaction_persistence_test.go index 7dd9b5a2..cabc7c66 100644 --- a/internal/mint/transaction_persistence_test.go +++ b/internal/mint/transaction_persistence_test.go @@ -150,7 +150,7 @@ func createMeltTestProofs(t *testing.T, amount uint64, activeKeys signer.GetKeys return proofs } -func TestSignAndSaveSigsPersistsIssuedState(t *testing.T) { +func TestSignMintOutputsAndMarkIssuedPersistsIssuedState(t *testing.T) { mint := SetupMintWithLightningMockPostgres(t) ctx := context.Background() @@ -192,9 +192,9 @@ func TestSignAndSaveSigsPersistsIssuedState(t *testing.T) { } outputs := createMintTestBlindedMessages(t, amount, activeKeys) - _, err = mint.signAndSaveSigs(ctx, cashu.PostMintBolt11Request{Signature: nil, Quote: mintRequest.Quote, Outputs: outputs}, mintRequest) + _, err = mint.signMintOutputsAndMarkIssued(ctx, cashu.PostMintBolt11Request{Signature: nil, Quote: mintRequest.Quote, Outputs: outputs}, mintRequest) if err != nil { - t.Fatalf("mint.signAndSaveSigs(ctx, request, mintRequest): %v", err) + t.Fatalf("mint.signMintOutputsAndMarkIssued(ctx, request, mintRequest): %v", err) } tx, err = mint.MintDB.GetTx(ctx) @@ -323,7 +323,7 @@ func TestSettleIfInternalMeltPersistsMintRequestState(t *testing.T) { } } -func TestValidateMeltStatusAndSpentPersistsProofQuoteReference(t *testing.T) { +func TestReserveMeltInputsAndMarkPendingPersistsProofQuoteReference(t *testing.T) { mint := SetupMintWithLightningMockPostgres(t) ctx := context.Background() @@ -372,9 +372,9 @@ func TestValidateMeltStatusAndSpentPersistsProofQuoteReference(t *testing.T) { Outputs: createMintTestBlindedMessages(t, 1, activeKeys), } - savedQuote, err := mint.validateMeltStatusAndSpent(ctx, request) + savedQuote, err := mint.reserveMeltInputsAndMarkPending(ctx, request) if err != nil { - t.Fatalf("mint.validateMeltStatusAndSpent(ctx, request): %v", err) + t.Fatalf("mint.reserveMeltInputsAndMarkPending(ctx, request): %v", err) } if savedQuote.State != cashu.PENDING { @@ -413,7 +413,7 @@ func TestValidateMeltStatusAndSpentPersistsProofQuoteReference(t *testing.T) { } } -func TestMeltQuoteUsesBackendAmountToSend(t *testing.T) { +func TestCreateMeltQuoteUsesBackendAmountToSend(t *testing.T) { mint := SetupMintWithLightningMockPostgres(t) mint.LightningBackend = quoteAmountBackend{ FakeWallet: lightning.FakeWallet{ @@ -428,13 +428,13 @@ func TestMeltQuoteUsesBackendAmountToSend(t *testing.T) { }, } - quote, err := mint.MeltQuote(context.Background(), cashu.PostMeltQuoteBolt11Request{ + quote, err := mint.CreateMeltQuote(context.Background(), cashu.PostMeltQuoteBolt11Request{ Options: cashu.PostMeltQuoteBolt11Options{Mpp: nil}, Request: RegtestRequest, Unit: cashu.Sat.String(), }, Bolt11) if err != nil { - t.Fatalf("mint.MeltQuote(context.Background(), request, Bolt11): %v", err) + t.Fatalf("mint.CreateMeltQuote(context.Background(), request, Bolt11): %v", err) } if quote.Amount != 2 { @@ -445,7 +445,7 @@ func TestMeltQuoteUsesBackendAmountToSend(t *testing.T) { } } -func TestSignAndSaveSigsSendsMintEvent(t *testing.T) { +func TestSignMintOutputsAndMarkIssuedSendsMintEvent(t *testing.T) { mint := SetupMintWithLightningMockPostgres(t) ctx := context.Background() @@ -490,9 +490,9 @@ func TestSignAndSaveSigsSendsMintEvent(t *testing.T) { mint.Observer.AddMintWatch(mintRequest.Quote, MintQuoteChannel{SubId: "mint-event", Channel: mintChan}) outputs := createMintTestBlindedMessages(t, amount, activeKeys) - _, err = mint.signAndSaveSigs(ctx, cashu.PostMintBolt11Request{Signature: nil, Quote: mintRequest.Quote, Outputs: outputs}, mintRequest) + _, err = mint.signMintOutputsAndMarkIssued(ctx, cashu.PostMintBolt11Request{Signature: nil, Quote: mintRequest.Quote, Outputs: outputs}, mintRequest) if err != nil { - t.Fatalf("mint.signAndSaveSigs(ctx, request, mintRequest): %v", err) + t.Fatalf("mint.signMintOutputsAndMarkIssued(ctx, request, mintRequest): %v", err) } select { diff --git a/internal/mint/utils_test.go b/internal/mint/utils_test.go index a3256338..599ae9c1 100644 --- a/internal/mint/utils_test.go +++ b/internal/mint/utils_test.go @@ -244,7 +244,7 @@ func TestVerifyOutputsFailRepeatedOutput(t *testing.T) { if err != nil { t.Fatalf("could not get tx: %+v", err) } - err = mint.CheckOutputSpent(tx, outputs) + err = mint.ValidateOutputsNotSpent(tx, outputs) if err == nil { t.Errorf("should have failed because of there are repeated outputs: %+v ", err) } diff --git a/internal/routes/auth.go b/internal/routes/auth.go index 194fe44b..456f5b43 100644 --- a/internal/routes/auth.go +++ b/internal/routes/auth.go @@ -113,7 +113,7 @@ func v1AuthRoutes(r *gin.Engine, mint *m.Mint) { return } - err = mint.CheckOutputSpent(tx, mintRequest.Outputs) + err = mint.ValidateOutputsNotSpent(tx, mintRequest.Outputs) if err != nil { slog.Warn("mint.VerifyOutputs(mintRequest.Outputs)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) diff --git a/internal/routes/bolt11.go b/internal/routes/bolt11.go index c21c40b3..644994c2 100644 --- a/internal/routes/bolt11.go +++ b/internal/routes/bolt11.go @@ -9,7 +9,7 @@ import ( "github.com/lescuer97/nutmix/internal/utils" ) -func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { +func registerV1Bolt11Routes(r *gin.Engine, mint *m.Mint) { v1 := r.Group("/v1") v1.POST("/mint/quote/bolt11", func(c *gin.Context) { @@ -24,7 +24,7 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { response, err := mint.CreateMintQuote(c.Request.Context(), mintRequest, m.Bolt11) if err != nil { - slog.Info("mint.Swap(c.Request.Context(), swapRequest)", slog.Any("error", err)) + slog.Info("mint.CreateMintQuote(c.Request.Context(), mintRequest, m.Bolt11)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return @@ -35,9 +35,9 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { v1.GET("/mint/quote/bolt11/:quote", func(c *gin.Context) { quoteId := c.Param("quote") - response, err := mint.MintQuoteStatus(c.Request.Context(), quoteId, m.Bolt11) + response, err := mint.RefreshMintQuoteStatus(c.Request.Context(), quoteId, m.Bolt11) if err != nil { - slog.Info("mint.MintQuoteStatus(c.Request.Context(), quoteId, m.Bolt11)", slog.Any("error", err)) + slog.Info("mint.RefreshMintQuoteStatus(c.Request.Context(), quoteId, m.Bolt11)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return @@ -55,9 +55,9 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - response, err := mint.Mint(c.Request.Context(), mintRequest, m.Bolt11) + response, err := mint.IssueTokens(c.Request.Context(), mintRequest, m.Bolt11) if err != nil { - slog.Info("Incorrect body", slog.Any("error", err)) + slog.Info("mint.IssueTokens(c.Request.Context(), mintRequest, m.Bolt11)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return @@ -75,9 +75,9 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { return } - dbRequest, err := mint.MeltQuote(c.Request.Context(), meltRequest, m.Bolt11) + dbRequest, err := mint.CreateMeltQuote(c.Request.Context(), meltRequest, m.Bolt11) if err != nil { - slog.Warn("mint.MeltQuote", slog.Any("error", err)) + slog.Warn("mint.CreateMeltQuote", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return @@ -88,9 +88,9 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { v1.GET("/melt/quote/bolt11/:quote", func(c *gin.Context) { quoteId := c.Param("quote") - quote, err := mint.CheckMeltQuoteState(c.Request.Context(), quoteId) + quote, err := mint.RefreshMeltQuoteState(c.Request.Context(), quoteId) if err != nil { - slog.Warn("mint.CheckMeltQuoteState(quoteId)", slog.Any("error", err)) + slog.Warn("mint.RefreshMeltQuoteState(quoteId)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return @@ -109,9 +109,9 @@ func v1bolt11Routes(r *gin.Engine, mint *m.Mint) { return } - quote, err := mint.Melt(c.Request.Context(), meltRequest, m.Bolt11) + quote, err := mint.ExecuteMelt(c.Request.Context(), meltRequest, m.Bolt11) if err != nil { - slog.Warn("mint.Melt(ctx, meltRequest)", slog.Any("error", err)) + slog.Warn("mint.ExecuteMelt(ctx, meltRequest)", slog.Any("error", err)) 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 a6a91d3b..35a46a05 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -9,7 +9,7 @@ import ( "github.com/lescuer97/nutmix/internal/utils" ) -func v1MintRoutes(r *gin.Engine, mint *m.Mint) { +func registerV1MintRoutes(r *gin.Engine, mint *m.Mint) { v1 := r.Group("/v1") v1.GET("/keys", func(c *gin.Context) { @@ -63,9 +63,9 @@ func v1MintRoutes(r *gin.Engine, mint *m.Mint) { return } - response, err := mint.Swap(c.Request.Context(), swapRequest) + response, err := mint.ExecuteSwap(c.Request.Context(), swapRequest) if err != nil { - slog.Info("mint.Swap(c.Request.Context(), swapRequest)", slog.Any("error", err)) + slog.Info("mint.ExecuteSwap(c.Request.Context(), swapRequest)", slog.Any("error", err)) errorCode, details := utils.ParseErrorToCashuErrorCode(err) c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return diff --git a/internal/routes/routes.go b/internal/routes/routes.go index f355822f..ce98e447 100644 --- a/internal/routes/routes.go +++ b/internal/routes/routes.go @@ -10,7 +10,7 @@ func V1Routes(r *gin.Engine, mint *mint.Mint) { r.Use(middleware.ClearAuthMiddleware(mint)) r.Use(middleware.BlindAuthMiddleware(mint)) v1AuthRoutes(r, mint) - v1MintRoutes(r, mint) - v1bolt11Routes(r, mint) + registerV1MintRoutes(r, mint) + registerV1Bolt11Routes(r, mint) v1WebSocketRoute(r, mint) } From 9c6acd3993ec6d1553c6b7a1340d1801ac0daf3f Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Mon, 20 Apr 2026 12:47:34 +0200 Subject: [PATCH 3/8] better proofs error --- api/cashu/errors.go | 2 + api/cashu/types.go | 1 - api/cashu/util.go | 2 +- internal/mint/mint.go | 6 ++- internal/mint/utils.go | 7 +-- internal/mint/utils_test.go | 44 +++++++++++++++++++ .../signer/local_signer/local_signer_test.go | 37 ++++++++++++++++ internal/signer/local_signer/signer.go | 16 +++++-- internal/utils/proofs.go | 4 ++ 9 files changed, 108 insertions(+), 11 deletions(-) diff --git a/api/cashu/errors.go b/api/cashu/errors.go index cfad6068..0f2cbc07 100644 --- a/api/cashu/errors.go +++ b/api/cashu/errors.go @@ -25,6 +25,8 @@ var ( ErrRequestNotPaid = errors.New("request not paid yet") ErrAmountlessInvoiceNotSupported = errors.New("Amount less invoices not supported") + + ErrKeysetNotKnow = errors.New("keyset not known") ) type ErrorCode uint diff --git a/api/cashu/types.go b/api/cashu/types.go index 8cd48601..5e3db173 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -27,7 +27,6 @@ var ( ErrCouldNotEncryptSeed = errors.New("could not encrypt seed") ErrCouldNotDecryptSeed = errors.New("could not decrypt seed") ErrKeysetNotFound = errors.New("keyset not found") - ErrKeysetForProofNotFound = errors.New("keyset for proof not found") ErrAlreadyActiveProof = errors.New("proof already being spent") ErrAlreadyActiveQuote = errors.New("quote already being spent") diff --git a/api/cashu/util.go b/api/cashu/util.go index 70c4219e..580bd799 100644 --- a/api/cashu/util.go +++ b/api/cashu/util.go @@ -64,7 +64,7 @@ func Fees(proofs []Proof, keysets []BasicKeysetResponse) (uint, error) { } } if keysetToUse.Id != proof.Id { - return 0, ErrKeysetForProofNotFound + return 0, ErrKeysetNotKnow } } diff --git a/internal/mint/mint.go b/internal/mint/mint.go index 0dee9c65..0832ce95 100644 --- a/internal/mint/mint.go +++ b/internal/mint/mint.go @@ -43,9 +43,11 @@ func checkProofsAreSameUnit(proofs []cashu.Proof, keys []cashu.BasicKeysetRespon for _, proof := range proofs { val, exists := seenKeys[proof.Id] - if exists { - units[val.Unit] = true + if !exists { + return cashu.Sat, cashu.ErrKeysetNotKnow } + + units[val.Unit] = true if len(units) > 1 { return cashu.Sat, cashu.ErrNotSameUnits } diff --git a/internal/mint/utils.go b/internal/mint/utils.go index 36f74b88..58e0ee21 100644 --- a/internal/mint/utils.go +++ b/internal/mint/utils.go @@ -50,10 +50,11 @@ func checkMessagesAreSameUnit(messages []cashu.BlindedMessage, keys []cashu.Basi } for _, proof := range messages { val, exists := seenKeys[proof.Id] - - if exists { - units[val.Unit] = true + if !exists { + return cashu.Sat, cashu.ErrKeysetNotKnow } + + units[val.Unit] = true if len(units) > 1 { return cashu.Sat, fmt.Errorf("proofs are not the same unit") } diff --git a/internal/mint/utils_test.go b/internal/mint/utils_test.go index 599ae9c1..3e791cba 100644 --- a/internal/mint/utils_test.go +++ b/internal/mint/utils_test.go @@ -210,6 +210,50 @@ func TestVerifyUnitOfProofPass(t *testing.T) { } } +func TestVerifyUnitOfProofFailsForUnknownKeyset(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + + keysets, err := mint.Signer.GetKeysets() + if err != nil { + t.Fatalf("mint.Signer.GetKeys(): %+v ", err) + } + + proofs := cashu.Proofs{ + {Id: keysets.Keysets[0].Id, Amount: 0}, + {Id: "missing-keyset", Amount: 0}, + } + + _, err = mint.CheckProofsAreSameUnit(proofs, keysets.Keysets) + if err == nil { + t.Fatal("expected missing keyset to fail") + } + if !errors.Is(err, cashu.ErrKeysetNotKnow) { + t.Errorf("Error should be keyset not known. %v", err) + } +} + +func TestVerifyOutputsFailsForUnknownKeyset(t *testing.T) { + mint := SetupMintWithLightningMockPostgres(t) + + keysets, err := mint.Signer.GetKeysets() + if err != nil { + t.Fatalf("mint.Signer.GetKeys(): %+v ", err) + } + + outputs := []cashu.BlindedMessage{ + {Id: keysets.Keysets[0].Id, Amount: 0}, + {Id: "missing-keyset", Amount: 0}, + } + + _, err = mint.VerifyOutputs(outputs, keysets.Keysets) + if err == nil { + t.Fatal("expected missing keyset to fail") + } + if !errors.Is(err, cashu.ErrKeysetNotKnow) { + t.Errorf("Error should be keyset not known. %v", err) + } +} + func TestVerifyOutputsFailRepeatedOutput(t *testing.T) { mint := SetupMintWithLightningMockPostgres(t) diff --git a/internal/signer/local_signer/local_signer_test.go b/internal/signer/local_signer/local_signer_test.go index 3dfbdf7d..29eb8d11 100644 --- a/internal/signer/local_signer/local_signer_test.go +++ b/internal/signer/local_signer/local_signer_test.go @@ -3,6 +3,7 @@ package localsigner import ( "context" "encoding/hex" + "errors" "strconv" "testing" "time" @@ -281,6 +282,42 @@ func TestMixedV1AndV2Keysets(t *testing.T) { } } +func TestSignBlindMessagesFailsForUnknownKeyset(t *testing.T) { + db := mockdb.MockDB{} //nolint:exhaustruct + t.Setenv("MINT_PRIVATE_KEY", MintPrivateKey) + + localsigner, err := SetupLocalSigner(&db) + if err != nil { + t.Fatalf("SetupLocalSigner(&db) %+v", err) + } + + _, _, err = localsigner.SignBlindMessages([]cashu.BlindedMessage{{Id: "missing-keyset", Amount: 1}}) + if err == nil { + t.Fatal("expected missing keyset to fail") + } + if !errors.Is(err, cashu.ErrKeysetNotKnow) { + t.Errorf("Error should be keyset not known. %v", err) + } +} + +func TestVerifyProofsFailsForUnknownKeyset(t *testing.T) { + db := mockdb.MockDB{} //nolint:exhaustruct + t.Setenv("MINT_PRIVATE_KEY", MintPrivateKey) + + localsigner, err := SetupLocalSigner(&db) + if err != nil { + t.Fatalf("SetupLocalSigner(&db) %+v", err) + } + + err = localsigner.VerifyProofs([]cashu.Proof{{Id: "missing-keyset", Amount: 1}}) + if err == nil { + t.Fatal("expected missing keyset to fail") + } + if !errors.Is(err, cashu.ErrKeysetNotKnow) { + t.Errorf("Error should be keyset not known. %v", err) + } +} + // Test Vectors Remote signer func TestSatConvertToInteger(t *testing.T) { intRef := parseUnitToIntegerReference(cashu.Sat.String()) diff --git a/internal/signer/local_signer/signer.go b/internal/signer/local_signer/signer.go index 2c299e9a..3945402d 100644 --- a/internal/signer/local_signer/signer.go +++ b/internal/signer/local_signer/signer.go @@ -279,9 +279,17 @@ func (l *LocalSigner) SignBlindMessages(messages []cashu.BlindedMessage) ([]cash var recoverSigDB = make([]cashu.RecoverSigDB, len(messages)) for i, output := range messages { - correctKeyset := l.activeKeysets[output.Id][output.Amount] + keysetsByAmount, exists := l.activeKeysets[output.Id] + if !exists { + return nil, nil, cashu.ErrKeysetNotKnow + } + + correctKeyset, exists := keysetsByAmount[output.Amount] + if !exists || correctKeyset.PrivKey == nil { + return nil, nil, cashu.ErrKeysetNotKnow + } - if correctKeyset.PrivKey == nil || !correctKeyset.Active { + if !correctKeyset.Active { return nil, nil, cashu.ErrUsingInactiveKeyset } @@ -324,7 +332,7 @@ func (l *LocalSigner) validateProof(proof cashu.Proof) error { keysets, exists := l.keysets[proof.Id] if !exists { - return cashu.ErrKeysetForProofNotFound + return cashu.ErrKeysetNotKnow } for _, keyset := range keysets { @@ -336,7 +344,7 @@ func (l *LocalSigner) validateProof(proof cashu.Proof) error { // check if keysetToUse is not assigned if keysetToUse.Id == "" { - return cashu.ErrKeysetForProofNotFound + return cashu.ErrKeysetNotKnow } verified := crypto.Verify(proof.Secret, keysetToUse.PrivKey, proof.C.PublicKey) if !verified { diff --git a/internal/utils/proofs.go b/internal/utils/proofs.go index 42b9c17b..b8333553 100644 --- a/internal/utils/proofs.go +++ b/internal/utils/proofs.go @@ -113,6 +113,10 @@ func ParseErrorToCashuErrorCode(proofError error) (cashu.ErrorCode, *string) { message := cashu.ErrBlindMessageAlreadySigned.Error() return cashu.OUTPUTS_ALREADY_SIGNED, &message + case errors.Is(proofError, cashu.ErrKeysetNotKnow): + message := cashu.ErrKeysetNotKnow.Error() + return cashu.KEYSET_NOT_KNOW, &message + case strings.Contains(proofError.Error(), "could not obtain lock"): message := "Transaction is already pending" return cashu.QUOTE_PENDING, &message From ea8c51c7b8fc7900d084e0a8209079345e57dcec Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Mon, 20 Apr 2026 13:42:09 +0200 Subject: [PATCH 4/8] fix lint --- internal/mint/utils_test.go | 38 +++++++++++++++++-- .../signer/local_signer/local_signer_test.go | 19 +++++++++- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/internal/mint/utils_test.go b/internal/mint/utils_test.go index 3e791cba..9147f267 100644 --- a/internal/mint/utils_test.go +++ b/internal/mint/utils_test.go @@ -219,8 +219,28 @@ func TestVerifyUnitOfProofFailsForUnknownKeyset(t *testing.T) { } proofs := cashu.Proofs{ - {Id: keysets.Keysets[0].Id, Amount: 0}, - {Id: "missing-keyset", Amount: 0}, + { + C: cashu.WrappedPublicKey{PublicKey: nil}, + Y: cashu.WrappedPublicKey{PublicKey: nil}, + Quote: nil, + Id: keysets.Keysets[0].Id, + Secret: "", + Witness: "", + State: "", + Amount: 0, + SeenAt: 0, + }, + { + C: cashu.WrappedPublicKey{PublicKey: nil}, + Y: cashu.WrappedPublicKey{PublicKey: nil}, + Quote: nil, + Id: "missing-keyset", + Secret: "", + Witness: "", + State: "", + Amount: 0, + SeenAt: 0, + }, } _, err = mint.CheckProofsAreSameUnit(proofs, keysets.Keysets) @@ -241,8 +261,18 @@ func TestVerifyOutputsFailsForUnknownKeyset(t *testing.T) { } outputs := []cashu.BlindedMessage{ - {Id: keysets.Keysets[0].Id, Amount: 0}, - {Id: "missing-keyset", Amount: 0}, + { + B_: cashu.WrappedPublicKey{PublicKey: nil}, + Id: keysets.Keysets[0].Id, + Witness: "", + Amount: 0, + }, + { + B_: cashu.WrappedPublicKey{PublicKey: nil}, + Id: "missing-keyset", + Witness: "", + Amount: 0, + }, } _, err = mint.VerifyOutputs(outputs, keysets.Keysets) diff --git a/internal/signer/local_signer/local_signer_test.go b/internal/signer/local_signer/local_signer_test.go index 29eb8d11..dc8b8258 100644 --- a/internal/signer/local_signer/local_signer_test.go +++ b/internal/signer/local_signer/local_signer_test.go @@ -291,7 +291,12 @@ func TestSignBlindMessagesFailsForUnknownKeyset(t *testing.T) { t.Fatalf("SetupLocalSigner(&db) %+v", err) } - _, _, err = localsigner.SignBlindMessages([]cashu.BlindedMessage{{Id: "missing-keyset", Amount: 1}}) + _, _, err = localsigner.SignBlindMessages([]cashu.BlindedMessage{{ + B_: cashu.WrappedPublicKey{PublicKey: nil}, + Id: "missing-keyset", + Witness: "", + Amount: 1, + }}) if err == nil { t.Fatal("expected missing keyset to fail") } @@ -309,7 +314,17 @@ func TestVerifyProofsFailsForUnknownKeyset(t *testing.T) { t.Fatalf("SetupLocalSigner(&db) %+v", err) } - err = localsigner.VerifyProofs([]cashu.Proof{{Id: "missing-keyset", Amount: 1}}) + err = localsigner.VerifyProofs([]cashu.Proof{{ + C: cashu.WrappedPublicKey{PublicKey: nil}, + Y: cashu.WrappedPublicKey{PublicKey: nil}, + Quote: nil, + Id: "missing-keyset", + Secret: "", + Witness: "", + State: "", + Amount: 1, + SeenAt: 0, + }}) if err == nil { t.Fatal("expected missing keyset to fail") } From 5ae0157a5e043facda5109cb60546b35baa9390b Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Tue, 28 Apr 2026 21:42:18 +0200 Subject: [PATCH 5/8] lint fixes --- api/cashu/types.go | 12 ++++++------ cmd/nutmix/main.go | 20 ++++++++------------ internal/database/postgresql/main.go | 6 +++--- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/api/cashu/types.go b/api/cashu/types.go index 5e3db173..25aedb56 100644 --- a/api/cashu/types.go +++ b/api/cashu/types.go @@ -181,18 +181,18 @@ func OrderedListOfPubkeys(listKeys []MintKey) []*secp256k1.PublicKey { return pubkeys } -type Seed struct { //nolint:govet - Amounts []uint64 `json:"amounts" db:"amounts"` +type Seed struct { + FinalExpiry *uint64 `json:"final_expiry" db:"final_expiry"` + IssuerVersion *string `json:"issuer_version" db:"issuer_version"` Unit string Id string - DerivationPath string `json:"derivation_path" db:"derivation_path"` + DerivationPath string `json:"derivation_path" db:"derivation_path"` + Amounts []uint64 `json:"amounts" db:"amounts"` CreatedAt int64 InputFeePpk uint `json:"input_fee_ppk" db:"input_fee_ppk"` Version uint32 Active bool - Legacy bool `json:"legacy" db:"legacy"` - FinalExpiry *uint64 `json:"final_expiry" db:"final_expiry"` - IssuerVersion *string `json:"issuer_version" db:"issuer_version"` + Legacy bool `json:"legacy" db:"legacy"` } type SwapMintMethod struct { diff --git a/cmd/nutmix/main.go b/cmd/nutmix/main.go index 7c1009dd..11f9adc6 100644 --- a/cmd/nutmix/main.go +++ b/cmd/nutmix/main.go @@ -66,10 +66,8 @@ func main() { w := io.MultiWriter(os.Stdout, logFile) - //nolint:exhaustruct - opts := &slog.HandlerOptions{ - Level: slog.LevelInfo, - } + opts := new(slog.HandlerOptions) + opts.Level = slog.LevelInfo err = godotenv.Load(".env") if err != nil { @@ -176,14 +174,12 @@ func main() { slog.Info("Nutmix started in port", slog.String("port", PORT)) // Define a custom http.Server - //nolint:exhaustruct - srv := &http.Server{ - Addr: PORT, - Handler: r, - ReadTimeout: 3 * time.Second, - WriteTimeout: 4 * time.Second, - IdleTimeout: 3 * time.Minute, - } + srv := new(http.Server) + srv.Addr = PORT + srv.Handler = r + srv.ReadTimeout = 3 * time.Second + srv.WriteTimeout = 4 * time.Second + srv.IdleTimeout = 3 * time.Minute // Start the server if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("server failed", slog.Any("error", err)) diff --git a/internal/database/postgresql/main.go b/internal/database/postgresql/main.go index d894d4f8..f582498f 100644 --- a/internal/database/postgresql/main.go +++ b/internal/database/postgresql/main.go @@ -153,7 +153,7 @@ func (pql Postgresql) SaveNewSeeds(seeds []cashu.Seed) error { func (pql Postgresql) UpdateSeedsActiveStatus(tx pgx.Tx, seeds []cashu.Seed) error { // change the paid status of the quote - batch := pgx.Batch{} //nolint:exhaustruct + var batch pgx.Batch for _, seed := range seeds { batch.Queue("UPDATE seeds SET active = $1 WHERE id = $2", seed.Active, seed.Id) } @@ -390,7 +390,7 @@ func (pql Postgresql) GetProofsFromQuote(tx pgx.Tx, quote string) (cashu.Proofs, } func (pql Postgresql) SetProofsState(tx pgx.Tx, proofs cashu.Proofs, state cashu.ProofState) error { // change the paid status of the quote - batch := pgx.Batch{} //nolint:exhaustruct + var batch pgx.Batch for _, proof := range proofs { batch.Queue(`UPDATE proofs SET state = $1 WHERE y = $2`, state, proof.Y) } @@ -414,7 +414,7 @@ func (pql Postgresql) SetProofsState(tx pgx.Tx, proofs cashu.Proofs, state cashu func (pql Postgresql) DeleteProofs(tx pgx.Tx, proofs cashu.Proofs) error { // change the paid status of the quote - batch := pgx.Batch{} //nolint:exhaustruct + var batch pgx.Batch for _, proof := range proofs { batch.Queue(`DELETE FROM proofs WHERE y = $1`, proof.Y) } From e663c7e7b54dd5d063516f4265048f50485d1d9c Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Tue, 28 Apr 2026 21:58:56 +0200 Subject: [PATCH 6/8] do it --- internal/database/mock_db/admin.go | 12 ------- internal/database/mock_db/main.go | 33 ------------------- internal/mint/config.go | 3 +- internal/routes/admin/handler.go | 3 +- internal/routes/admin/mint-activity.go | 1 - internal/routes/admin/pages.go | 4 --- internal/routes/admin/slog_nostr_handler.go | 1 - .../routes/admin/slog_nostr_handler_test.go | 5 ++- internal/routes/admin/tabs.go | 5 ++- internal/utils/nostr_notification_secret.go | 1 - 10 files changed, 7 insertions(+), 61 deletions(-) diff --git a/internal/database/mock_db/admin.go b/internal/database/mock_db/admin.go index 481a1a69..0e6b9daf 100644 --- a/internal/database/mock_db/admin.go +++ b/internal/database/mock_db/admin.go @@ -15,7 +15,6 @@ import ( func (m *MockDB) SaveNostrAuth(auth database.NostrLoginAuth) error { return nil - } func (m *MockDB) UpdateNostrAuthActivation(tx pgx.Tx, nonce string, activated bool) error { @@ -25,28 +24,22 @@ func (m *MockDB) UpdateNostrAuthActivation(tx pgx.Tx, nonce string, activated bo func (m *MockDB) GetNostrAuth(tx pgx.Tx, 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) AddLiquiditySwap(tx pgx.Tx, swap utils.LiquiditySwap) error { m.LiquiditySwap = append(m.LiquiditySwap, swap) return nil - } func (m *MockDB) ChangeLiquiditySwapState(tx pgx.Tx, id string, state utils.SwapState) error { for i := 0; i < len(m.LiquiditySwap); i++ { if m.LiquiditySwap[i].Id == id { m.LiquiditySwap[i].State = state } - } return nil @@ -55,12 +48,9 @@ func (m *MockDB) ChangeLiquiditySwapState(tx pgx.Tx, id string, state utils.Swap func (m *MockDB) GetLiquiditySwapById(tx pgx.Tx, id string) (utils.LiquiditySwap, error) { var liquiditySwaps []utils.LiquiditySwap for i := 0; i < len(m.LiquiditySwap); i++ { - if m.LiquiditySwap[i].Id == id { liquiditySwaps = append(liquiditySwaps, m.LiquiditySwap[i]) - } - } return liquiditySwaps[0], nil @@ -76,11 +66,9 @@ func (m *MockDB) GetLiquiditySwapsByStates(tx pgx.Tx, states []utils.SwapState) if slices.Contains(states, m.LiquiditySwap[i].State) { liquiditySwaps = append(liquiditySwaps, m.LiquiditySwap[i].Id) } - } return liquiditySwaps, nil - } func cloneStringPtr(value *string) *string { diff --git a/internal/database/mock_db/main.go b/internal/database/mock_db/main.go index 840a305e..692b4793 100644 --- a/internal/database/mock_db/main.go +++ b/internal/database/mock_db/main.go @@ -61,12 +61,9 @@ func (m *MockDB) Rollback(ctx context.Context, tx pgx.Tx) error { func (m *MockDB) GetSeedsByUnit(tx pgx.Tx, unit cashu.Unit) ([]cashu.Seed, error) { 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 } @@ -89,7 +86,6 @@ func (m *MockDB) UpdateSeedsActiveStatus(tx pgx.Tx, seeds []cashu.Seed) error { break } } - } return nil @@ -106,7 +102,6 @@ func (m *MockDB) ChangeMintRequestState(tx pgx.Tx, quote string, state cashu.ACT m.MintRequest[i].State = state m.MintRequest[i].Minted = minted } - } return nil } @@ -114,12 +109,9 @@ func (m *MockDB) ChangeMintRequestState(tx pgx.Tx, quote string, state cashu.ACT func (m *MockDB) GetMintRequestById(tx pgx.Tx, 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]) - } - } if len(mintRequests) == 0 { return cashu.MintRequestDB{}, pgx.ErrNoRows @@ -145,7 +137,6 @@ func (m *MockDB) GetMintRequestByRequest(tx pgx.Tx, request string) (cashu.MintR func (m *MockDB) GetMeltRequestById(tx pgx.Tx, id string) (cashu.MeltRequestDB, error) { var meltRequests []cashu.MeltRequestDB for i := 0; i < len(m.MeltRequest); i++ { - if m.MeltRequest[i].Quote == id { meltRequests = append(meltRequests, m.MeltRequest[i]) } @@ -159,10 +150,8 @@ func (m *MockDB) GetMeltRequestById(tx pgx.Tx, id string) (cashu.MeltRequestDB, func (m *MockDB) GetMeltQuotesByState(state cashu.ACTION_STATE) ([]cashu.MeltRequestDB, error) { var meltRequests []cashu.MeltRequestDB for i := 0; i < len(m.MeltRequest); i++ { - if m.MeltRequest[i].State == state { meltRequests = append(meltRequests, m.MeltRequest[i]) - } } if len(meltRequests) == 0 { @@ -173,11 +162,9 @@ func (m *MockDB) GetMeltQuotesByState(state cashu.ACTION_STATE) ([]cashu.MeltReq } func (m *MockDB) SaveMeltRequest(tx pgx.Tx, request cashu.MeltRequestDB) error { - m.MeltRequest = append(m.MeltRequest, request) return nil - } func (m *MockDB) AddPreimageMeltRequest(tx pgx.Tx, preimage string, quote string) error { @@ -185,10 +172,8 @@ func (m *MockDB) AddPreimageMeltRequest(tx pgx.Tx, preimage string, quote string if m.MeltRequest[i].Quote == quote { m.MeltRequest[i].PaymentPreimage = preimage } - } return nil - } func (m *MockDB) ChangeMeltRequestState(tx pgx.Tx, quote string, state cashu.ACTION_STATE, melted bool, paid_fee uint64) error { for i := 0; i < len(m.MeltRequest); i++ { @@ -197,7 +182,6 @@ func (m *MockDB) ChangeMeltRequestState(tx pgx.Tx, quote string, state cashu.ACT m.MeltRequest[i].Melted = melted m.MeltRequest[i].FeePaid = paid_fee } - } return nil } @@ -206,7 +190,6 @@ func (m *MockDB) ChangeCheckingId(tx pgx.Tx, quote string, checking_id string) e if m.MeltRequest[i].Quote == quote { m.MeltRequest[i].CheckingId = checking_id } - } return nil } @@ -214,18 +197,13 @@ func (m *MockDB) ChangeCheckingId(tx pgx.Tx, quote string, checking_id string) e func (m *MockDB) GetProofsFromSecret(tx pgx.Tx, SecretList []string) (cashu.Proofs, error) { var proofs cashu.Proofs 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 @@ -234,11 +212,9 @@ func (m *MockDB) GetProofsFromQuote(tx pgx.Tx, quote string) (cashu.Proofs, erro var proofs cashu.Proofs for j := 0; j < len(m.Proofs); j++ { - if m.Proofs[j].Quote != nil { if quote == *m.Proofs[j].Quote { proofs = append(proofs, m.Proofs[j]) - } } } @@ -249,24 +225,18 @@ func (m *MockDB) GetProofsFromQuote(tx pgx.Tx, quote string) (cashu.Proofs, erro func (m *MockDB) SaveProof(tx pgx.Tx, proofs []cashu.Proof) error { m.Proofs = append(m.Proofs, proofs...) return nil - } func (m *MockDB) GetProofsFromSecretCurve(tx pgx.Tx, Ys []cashu.WrappedPublicKey) (cashu.Proofs, error) { var proofs cashu.Proofs 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 @@ -286,9 +256,7 @@ func (m *MockDB) DeleteProofs(tx pgx.Tx, proofs cashu.Proofs) error { func (m *MockDB) SetProofsState(tx pgx.Tx, proofs cashu.Proofs, state cashu.ProofState) error { for i := 0; i < len(m.Proofs); i++ { - for j := 0; j < len(proofs); j++ { - if proofs[j].Secret == m.Proofs[i].Secret { m.Proofs[i].State = state } @@ -314,5 +282,4 @@ func (m *MockDB) GetRestoreSigsFromBlindedMessages(tx pgx.Tx, B_ []cashu.Wrapped func (m *MockDB) SaveRestoreSigs(tx pgx.Tx, recover_sigs []cashu.RecoverSigDB) error { m.RecoverSigDB = append(m.RecoverSigDB, recover_sigs...) return nil - } diff --git a/internal/mint/config.go b/internal/mint/config.go index 9882ecaf..53770c00 100644 --- a/internal/mint/config.go +++ b/internal/mint/config.go @@ -69,18 +69,19 @@ func SetUpConfigDB(ctx context.Context, db database.MintDB) (utils.Config, *util return config, nil, fmt.Errorf("getConfigFile(), %w", err) } + //nolint:musttag // Config intentionally reuses db-tagged struct for bootstrap config file values. err = toml.Unmarshal(file, &config) if err != nil { return config, nil, fmt.Errorf("toml.Unmarshal(buf,&config), %w", err) } + //nolint:musttag // Bootstrap struct fields map directly to legacy uppercase config keys. err = toml.Unmarshal(file, &fileNostrConfig) if err != nil { return config, nil, fmt.Errorf("toml.Unmarshal(buf,&fileNostrConfig): %w", err) } switch { - // if no config set default to toml case (len(config.NETWORK) == 0 && len(config.MINT_LIGHTNING_BACKEND) == 0): config.Default() diff --git a/internal/routes/admin/handler.go b/internal/routes/admin/handler.go index c0bfc578..43673936 100644 --- a/internal/routes/admin/handler.go +++ b/internal/routes/admin/handler.go @@ -92,7 +92,7 @@ func (a *adminHandler) getKeysets(filterUnits []string) (map[string][]templates. } // create ordered list of units - var orderedUnits []string + orderedUnits := make([]string, 0, len(keysetMap)) for unit := range keysetMap { orderedUnits = append(orderedUnits, unit) } @@ -132,7 +132,6 @@ func (a *adminHandler) EcashBalance(since time.Time) (templates.Balance, error) return templates.Balance{}, fmt.Errorf("a.mint.MintDB.GetStatsSnapshotsBySince(context.Background(), since.Unix()). %w", err) } return balanceFromStatsSnapshots(statsRows), nil - } func balanceFromStatsSnapshots(rows []database.StatsSnapshot) templates.Balance { diff --git a/internal/routes/admin/mint-activity.go b/internal/routes/admin/mint-activity.go index 0505ca5a..fd6e2637 100644 --- a/internal/routes/admin/mint-activity.go +++ b/internal/routes/admin/mint-activity.go @@ -13,7 +13,6 @@ import ( func SwapsList(mint *m.Mint) gin.HandlerFunc { return func(c *gin.Context) { - swaps, err := mint.MintDB.GetAllLiquiditySwaps() if err != nil { diff --git a/internal/routes/admin/pages.go b/internal/routes/admin/pages.go index 286e4751..cc45a6a5 100644 --- a/internal/routes/admin/pages.go +++ b/internal/routes/admin/pages.go @@ -46,7 +46,6 @@ func LoginPage(mint *mint.Mint, adminNostrKeyAvailable bool) gin.HandlerFunc { slog.String(utils.LogExtraInfo, err.Error())) _ = c.Error(err) return - } ctx := c.Request.Context() @@ -55,7 +54,6 @@ func LoginPage(mint *mint.Mint, adminNostrKeyAvailable bool) gin.HandlerFunc { _ = c.Error(err) return } - } } @@ -287,7 +285,6 @@ func SwapStatusPage(mint *mint.Mint) gin.HandlerFunc { if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { slog.Error("Failed to rollback transaction", slog.Any("error", rollbackErr)) } - } else if err != nil { _ = c.Error(fmt.Errorf("rolling back because of failure %+v", err)) if rollbackErr := mint.MintDB.Rollback(ctx, tx); rollbackErr != nil { @@ -319,7 +316,6 @@ func SwapStatusPage(mint *mint.Mint) gin.HandlerFunc { component = templates.LightningReceiveSummary(amount, swap.LightningInvoice, qrcode, swap.Id) case utils.LiquidityOut: component = templates.LightningSendSummary(amount, swap.LightningInvoice, swap.Id) - } err = templates.SwapStatusPage(component).Render(ctx, c.Writer) diff --git a/internal/routes/admin/slog_nostr_handler.go b/internal/routes/admin/slog_nostr_handler.go index 2c0761db..574fd518 100644 --- a/internal/routes/admin/slog_nostr_handler.go +++ b/internal/routes/admin/slog_nostr_handler.go @@ -71,7 +71,6 @@ func (h *NostrErrorNotifyHandler) Handle(ctx context.Context, record slog.Record ctx, cancel := context.WithTimeout(parentCtx, 7*time.Second) defer cancel() for _, pubkey := range h.mint.NostrNotificationConfig.NOSTR_NOTIFICATION_NPUBS { - _ = h.SendPrivateNostrMessage(ctx, pubkey, payload) } }(ctx, message) diff --git a/internal/routes/admin/slog_nostr_handler_test.go b/internal/routes/admin/slog_nostr_handler_test.go index 1c0e2f9c..a603eb05 100644 --- a/internal/routes/admin/slog_nostr_handler_test.go +++ b/internal/routes/admin/slog_nostr_handler_test.go @@ -1,7 +1,6 @@ package admin import ( - "io" "log/slog" "testing" "time" @@ -22,7 +21,7 @@ func TestFormatRecordForNostrIncludesSortedAttrs(t *testing.T) { } func TestNewNostrErrorNotifyHandlerCreatesHandlerWhenNotificationsDisabled(t *testing.T) { - base := slog.NewTextHandler(io.Discard, nil) + base := slog.DiscardHandler var mintValue m.Mint var nostrNotificationConfig utils.NostrNotificationConfig mintValue.NostrNotificationConfig = &nostrNotificationConfig @@ -34,7 +33,7 @@ func TestNewNostrErrorNotifyHandlerCreatesHandlerWhenNotificationsDisabled(t *te } func TestNewNostrErrorNotifyHandlerCreatesHandlerWhenNip04DmDisabled(t *testing.T) { - base := slog.NewTextHandler(io.Discard, nil) + base := slog.DiscardHandler var mintValue m.Mint var nostrNotificationConfig utils.NostrNotificationConfig nostrNotificationConfig.NOSTR_NOTIFICATIONS = true diff --git a/internal/routes/admin/tabs.go b/internal/routes/admin/tabs.go index 16f25632..b0e51032 100644 --- a/internal/routes/admin/tabs.go +++ b/internal/routes/admin/tabs.go @@ -330,7 +330,6 @@ func MintSettingsLightning(mint *m.Mint) gin.HandlerFunc { pegoutOnly := c.Request.PostFormValue("PEG_OUT_ONLY") if pegoutOnly == "on" { mint.Config.PEG_OUT_ONLY = true - } else { mint.Config.PEG_OUT_ONLY = false } @@ -746,7 +745,6 @@ func Bolt11Post(mint *m.Mint) gin.HandlerFunc { ) switch c.Request.PostFormValue("MINT_LIGHTNING_BACKEND") { - case string(utils.FAKE_WALLET): newBackendType = utils.FAKE_WALLET fakeWalletBackend := lightning.FakeWallet{ @@ -862,9 +860,10 @@ func Bolt11Post(mint *m.Mint) gin.HandlerFunc { // We use a dummy quote ID to avoid messing with real DB if possible. testQuote := "verification-test-" + strconv.FormatInt(time.Now().Unix(), 10) //nolint:exhaustruct + testDescription := testQuote invoiceResp, err := newBackend.RequestInvoice( - cashu.MintRequestDB{Quote: testQuote}, cashu.NewAmount(cashu.Sat, 100), + &testDescription, ) if err != nil { slog.Warn("newBackend.RequestInvoice failed during verification", slog.String("err", err.Error())) diff --git a/internal/utils/nostr_notification_secret.go b/internal/utils/nostr_notification_secret.go index b91a879e..acb4a024 100644 --- a/internal/utils/nostr_notification_secret.go +++ b/internal/utils/nostr_notification_secret.go @@ -104,7 +104,6 @@ func ReadNostrNotificationNsec() ([]byte, error) { } func ReadNostrNotificationNsecFromDir(dirPath string) ([]byte, error) { - nsecPath := nostrNotificationNsecFilePath(dirPath) fileInfo, err := os.Lstat(nsecPath) if err != nil { From 27d52359dfbab0430240e7392a286c64a5cb5095 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Mon, 18 May 2026 17:30:27 +0200 Subject: [PATCH 7/8] fix lnbits --- internal/lightning/lnbits.go | 43 ++++++++++++----------------- internal/lightning/lnbits_test.go | 46 +++++++++++++++++++++++++++++++ internal/mint/melting.go | 1 - internal/mint/minting.go | 3 -- 4 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 internal/lightning/lnbits_test.go diff --git a/internal/lightning/lnbits.go b/internal/lightning/lnbits.go index 683715bd..b4723682 100644 --- a/internal/lightning/lnbits.go +++ b/internal/lightning/lnbits.go @@ -57,6 +57,21 @@ type lnbitsFeeResponse struct { var ErrLnbitsFailedPayment = errors.New("failed payment") var ErrLnBitsNoRouteFound = errors.New("no route found") +func lnbitsPaymentState(paymentStatus LNBitsPaymentStatus) PaymentStatus { + switch { + case paymentStatus.Paid: + return SETTLED + case paymentStatus.Pending: + return PENDING + case paymentStatus.Details.Status == "pending": + return PENDING + case paymentStatus.Details.Pending: + return PENDING + default: + return FAILED + } +} + func (l *LnbitsWallet) LnbitsRequest(method string, endpoint string, reqBody any, responseType any) error { client := &http.Client{} //nolint:exhaustruct jsonBytes, err := json.Marshal(reqBody) @@ -161,7 +176,7 @@ func (l LnbitsWallet) PayInvoice(melt_quote cashu.MeltRequestDB, zpayInvoice *zp } invoiceRes.PaymentRequest = lnbitsInvoice.PaymentRequest - invoiceRes.PaymentState = SETTLED + invoiceRes.PaymentState = lnbitsPaymentState(paymentStatus) invoiceRes.Rhash = lnbitsInvoice.PaymentHash invoiceRes.Preimage = paymentStatus.Preimage // LNBits returns fee as int64, convert to Amount @@ -183,20 +198,7 @@ func (l LnbitsWallet) CheckPayed(quote string, invoice *zpay32.Invoice, checking fee := cashu.Amount{Unit: cashu.Sat, Amount: uint64(paymentStatus.Details.Fee)} - switch { - case paymentStatus.Paid: - return SETTLED, paymentStatus.Preimage, fee, nil - case paymentStatus.Details.Status == "pending": - return PENDING, paymentStatus.Preimage, fee, nil - case paymentStatus.Details.Pending: - return PENDING, paymentStatus.Preimage, fee, nil - case !paymentStatus.Paid && paymentStatus.Details.Status == "failed": - return FAILED, paymentStatus.Preimage, fee, nil - case !paymentStatus.Paid && !paymentStatus.Details.Pending: - return FAILED, paymentStatus.Preimage, fee, nil - default: - return FAILED, paymentStatus.Preimage, fee, nil - } + return lnbitsPaymentState(paymentStatus), paymentStatus.Preimage, fee, nil } func (l LnbitsWallet) CheckReceived(quote cashu.MintRequestDB, invoice *zpay32.Invoice) (PaymentStatus, string, error) { @@ -209,16 +211,7 @@ func (l LnbitsWallet) CheckReceived(quote cashu.MintRequestDB, invoice *zpay32.I return FAILED, "", fmt.Errorf("json.Marshal: %w", err) } - switch { - case paymentStatus.Paid: - return SETTLED, paymentStatus.Preimage, nil - case paymentStatus.Details.Pending: - return PENDING, paymentStatus.Preimage, nil - case !paymentStatus.Paid && !paymentStatus.Details.Pending: - return FAILED, paymentStatus.Preimage, nil - default: - return FAILED, paymentStatus.Preimage, nil - } + return lnbitsPaymentState(paymentStatus), paymentStatus.Preimage, nil } func (l LnbitsWallet) QueryFees(invoice string, zpayInvoice *zpay32.Invoice, mpp bool, amount cashu.Amount) (FeesResponse, error) { diff --git a/internal/lightning/lnbits_test.go b/internal/lightning/lnbits_test.go new file mode 100644 index 00000000..301dbde4 --- /dev/null +++ b/internal/lightning/lnbits_test.go @@ -0,0 +1,46 @@ +package lightning + +import "testing" + +func TestLnbitsPaymentState(t *testing.T) { + tests := []struct { + name string + status LNBitsPaymentStatus + wanted PaymentStatus + }{ + { + name: "paid status settles payment", + status: LNBitsPaymentStatus{Paid: true}, + wanted: SETTLED, + }, + { + name: "top level pending keeps payment pending", + status: LNBitsPaymentStatus{Pending: true}, + wanted: PENDING, + }, + { + name: "detail status pending keeps payment pending", + status: LNBitsPaymentStatus{Details: LNBitsPaymentStatusDetail{Status: "pending"}}, + wanted: PENDING, + }, + { + name: "detail pending keeps payment pending", + status: LNBitsPaymentStatus{Details: LNBitsPaymentStatusDetail{Pending: true}}, + wanted: PENDING, + }, + { + name: "unpaid non pending fails payment", + status: LNBitsPaymentStatus{}, + wanted: FAILED, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := lnbitsPaymentState(test.status) + if got != test.wanted { + t.Fatalf("lnbitsPaymentState() = %v, want %v", got, test.wanted) + } + }) + } +} diff --git a/internal/mint/melting.go b/internal/mint/melting.go index faf19599..2cb25444 100644 --- a/internal/mint/melting.go +++ b/internal/mint/melting.go @@ -62,7 +62,6 @@ func (m *Mint) createAndStoreBolt11MeltQuote(ctx context.Context, meltRequest ca queryFee = feesResponse.Fees.Amount amountToSend = feesResponse.AmountToSend } - //FIXME: Add method dbRequest := cashu.MeltRequestDB{ Amount: amountToSend.Amount, Quote: quoteId, diff --git a/internal/mint/minting.go b/internal/mint/minting.go index 455d07d7..0c67d5b4 100644 --- a/internal/mint/minting.go +++ b/internal/mint/minting.go @@ -112,7 +112,6 @@ func (m *Mint) createBolt11MintQuote(ctx context.Context, request cashu.PostMint return mintRequestDB.PostMintQuoteBolt11Response(), nil } -// FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration func (m *Mint) RefreshMintQuoteStatus(ctx context.Context, quoteId string, method METHOD) (cashu.PostMintQuoteBolt11Response, error) { tx, err := m.MintDB.GetTx(ctx) if err != nil { @@ -150,7 +149,6 @@ func (m *Mint) RefreshMintQuoteStatus(ctx context.Context, quoteId string, metho } } -// FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration func (m *Mint) reconcileBolt11MintQuoteState(ctx context.Context, request cashu.MintRequestDB, method METHOD) (cashu.MintRequestDB, error) { if method != Bolt11 { return cashu.MintRequestDB{}, fmt.Errorf("request method is not BOLT11") @@ -286,7 +284,6 @@ func (m *Mint) loadAndValidateMintQuoteForIssuance(ctx context.Context, request return quote, nil } -// FIXME: the method should be inside the MintRequestDB struct. this needs to change in the db and add a migration func (m *Mint) bolt11Mint(ctx context.Context, request cashu.PostMintBolt11Request, mintReq cashu.MintRequestDB, method METHOD) (cashu.PostMintBolt11Response, error) { if method != Bolt11 { return cashu.PostMintBolt11Response{}, fmt.Errorf("request method is not BOLT11") From fba9aeeb1ce77281a0e6c369f1d3a39352581e41 Mon Sep 17 00:00:00 2001 From: lescuer97 Date: Tue, 19 May 2026 21:14:04 +0200 Subject: [PATCH 8/8] add observer --- internal/lightning/lnbits_test.go | 70 ++++++++++++++++++++++++++----- internal/mint/minting.go | 2 + internal/mint/swapping.go | 2 + internal/routes/mint.go | 1 - 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/internal/lightning/lnbits_test.go b/internal/lightning/lnbits_test.go index 301dbde4..15af7fd7 100644 --- a/internal/lightning/lnbits_test.go +++ b/internal/lightning/lnbits_test.go @@ -9,28 +9,78 @@ func TestLnbitsPaymentState(t *testing.T) { wanted PaymentStatus }{ { - name: "paid status settles payment", - status: LNBitsPaymentStatus{Paid: true}, + name: "paid status settles payment", + status: LNBitsPaymentStatus{ + Preimage: "", + Details: LNBitsPaymentStatusDetail{ + Memo: "", + Status: "", + Fee: 0, + Pending: false, + }, + Paid: true, + Pending: false, + }, wanted: SETTLED, }, { - name: "top level pending keeps payment pending", - status: LNBitsPaymentStatus{Pending: true}, + name: "top level pending keeps payment pending", + status: LNBitsPaymentStatus{ + Preimage: "", + Details: LNBitsPaymentStatusDetail{ + Memo: "", + Status: "", + Fee: 0, + Pending: false, + }, + Paid: false, + Pending: true, + }, wanted: PENDING, }, { - name: "detail status pending keeps payment pending", - status: LNBitsPaymentStatus{Details: LNBitsPaymentStatusDetail{Status: "pending"}}, + name: "detail status pending keeps payment pending", + status: LNBitsPaymentStatus{ + Preimage: "", + Details: LNBitsPaymentStatusDetail{ + Memo: "", + Status: "pending", + Fee: 0, + Pending: false, + }, + Paid: false, + Pending: false, + }, wanted: PENDING, }, { - name: "detail pending keeps payment pending", - status: LNBitsPaymentStatus{Details: LNBitsPaymentStatusDetail{Pending: true}}, + name: "detail pending keeps payment pending", + status: LNBitsPaymentStatus{ + Preimage: "", + Details: LNBitsPaymentStatusDetail{ + Memo: "", + Status: "", + Fee: 0, + Pending: true, + }, + Paid: false, + Pending: false, + }, wanted: PENDING, }, { - name: "unpaid non pending fails payment", - status: LNBitsPaymentStatus{}, + name: "unpaid non pending fails payment", + status: LNBitsPaymentStatus{ + Preimage: "", + Details: LNBitsPaymentStatusDetail{ + Memo: "", + Status: "", + Fee: 0, + Pending: false, + }, + Paid: false, + Pending: false, + }, wanted: FAILED, }, } diff --git a/internal/mint/minting.go b/internal/mint/minting.go index 0c67d5b4..40d0a93d 100644 --- a/internal/mint/minting.go +++ b/internal/mint/minting.go @@ -330,6 +330,8 @@ func (m *Mint) bolt11Mint(ctx context.Context, request cashu.PostMintBolt11Reque if err != nil { return cashu.PostMintBolt11Response{}, err } + + go m.Observer.SendMintEvent(mintReq) return cashu.PostMintBolt11Response{Signatures: blindSigs}, nil } diff --git a/internal/mint/swapping.go b/internal/mint/swapping.go index 450bd163..ea0a09c7 100644 --- a/internal/mint/swapping.go +++ b/internal/mint/swapping.go @@ -63,6 +63,8 @@ func (m *Mint) ExecuteSwap(ctx context.Context, request cashu.PostSwapRequest) ( return cashu.PostSwapResponse{}, fmt.Errorf("m.signSwapOutputsAndMarkInputsSpent(ctx, proofs, request). %w", err) } + proofs.SetProofsState(cashu.PROOF_SPENT) + go m.Observer.SendProofsEvent(proofs) // mark as pending and sign return cashu.PostSwapResponse{ Signatures: blindSignatures, diff --git a/internal/routes/mint.go b/internal/routes/mint.go index 35a46a05..0fe7d1ae 100644 --- a/internal/routes/mint.go +++ b/internal/routes/mint.go @@ -70,7 +70,6 @@ func registerV1MintRoutes(r *gin.Engine, mint *m.Mint) { c.JSON(400, cashu.ErrorCodeToResponse(errorCode, details)) return } - go mint.Observer.SendProofsEvent(swapRequest.Inputs) c.JSON(200, response) })