diff --git a/cashu/cashu.go b/cashu/cashu.go index 435e71e..0eb0735 100644 --- a/cashu/cashu.go +++ b/cashu/cashu.go @@ -473,11 +473,13 @@ const ( MintQuoteRequestNotPaidErrCode CashuErrCode = 20001 MintQuoteAlreadyIssuedErrCode CashuErrCode = 20002 MintingDisabledErrCode CashuErrCode = 20003 + MintQuoteInvalidSigErrCode CashuErrCode = 20008 MeltQuotePendingErrCode CashuErrCode = 20005 MeltQuoteAlreadyPaidErrCode CashuErrCode = 20006 - LightningPaymentErrCode CashuErrCode = 20008 - MeltQuoteErrCode CashuErrCode = 20009 + + //LightningPaymentErrCode CashuErrCode = 20008 + MeltQuoteErrCode CashuErrCode = 20009 ) var ( @@ -492,6 +494,7 @@ var ( MintQuoteAlreadyIssued = Error{Detail: "quote already issued", Code: MintQuoteAlreadyIssuedErrCode} MintingDisabled = Error{Detail: "minting is disabled", Code: MintingDisabledErrCode} MintAmountExceededErr = Error{Detail: "max amount for minting exceeded", Code: AmountLimitExceeded} + MintQuoteInvalidSigErr = Error{Detail: "Mint quote with pubkey but no valid signature provided.", Code: MintQuoteInvalidSigErrCode} OutputsOverQuoteAmountErr = Error{Detail: "sum of the output amounts is greater than quote amount", Code: StandardErrCode} ProofAlreadyUsedErr = Error{Detail: "proof already used", Code: ProofAlreadyUsedErrCode} ProofPendingErr = Error{Detail: "proof is pending", Code: ProofAlreadyUsedErrCode} diff --git a/cashu/nuts/nut06/nut06.go b/cashu/nuts/nut06/nut06.go index 6e76d99..65c4fb0 100644 --- a/cashu/nuts/nut06/nut06.go +++ b/cashu/nuts/nut06/nut06.go @@ -91,6 +91,7 @@ type Nuts struct { Nut14 Supported `json:"14"` Nut15 *NutSetting `json:"15,omitempty"` Nut17 nut17.InfoSetting `json:"17"` + Nut20 Supported `json:"20"` } // custom unmarshaller because format to signal support for nut-15 changed. @@ -109,6 +110,7 @@ func (nuts *Nuts) UnmarshalJSON(data []byte) error { Nut14 Supported `json:"14"` Nut15 json.RawMessage `json:"15,omitempty"` Nut17 nut17.InfoSetting `json:"17"` + Nut20 Supported `json:"20"` } if err := json.Unmarshal(data, &tempNuts); err != nil { @@ -125,6 +127,7 @@ func (nuts *Nuts) UnmarshalJSON(data []byte) error { nuts.Nut12 = tempNuts.Nut12 nuts.Nut14 = tempNuts.Nut14 nuts.Nut17 = tempNuts.Nut17 + nuts.Nut20 = tempNuts.Nut20 if err := json.Unmarshal(tempNuts.Nut15, &nuts.Nut15); err != nil { var nut15Methods []MethodSetting diff --git a/cashu/nuts/nut20/nut20.go b/cashu/nuts/nut20/nut20.go index 42d267d..7289439 100644 --- a/cashu/nuts/nut20/nut20.go +++ b/cashu/nuts/nut20/nut20.go @@ -26,3 +26,18 @@ func SignMintQuote( return sig, nil } + +func VerifyMintQuoteSignature( + signature *schnorr.Signature, + quoteId string, + blindedMessages cashu.BlindedMessages, + publicKey *secp256k1.PublicKey, +) bool { + msg := quoteId + for _, bm := range blindedMessages { + msg += bm.B_ + } + hash := sha256.Sum256([]byte(msg)) + + return signature.Verify(hash[:], publicKey) +} diff --git a/cashu/nuts/nut20/nut20_test.go b/cashu/nuts/nut20/nut20_test.go new file mode 100644 index 0000000..74ab625 --- /dev/null +++ b/cashu/nuts/nut20/nut20_test.go @@ -0,0 +1,154 @@ +package nut20 + +import ( + "encoding/hex" + "testing" + + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/elnosh/gonuts/cashu" +) + +func TestSignMintQuote(t *testing.T) { + privateKey, _ := secp256k1.GeneratePrivateKey() + + tests := []struct { + quoteId string + outputs cashu.BlindedMessages + privateKey *secp256k1.PrivateKey + }{ + { + quoteId: "9d745270-1405-46de-b5c5-e2762b4f5e00", + outputs: cashu.BlindedMessages{ + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79", + }, + }, + privateKey: privateKey, + }, + } + + for _, test := range tests { + sig, err := SignMintQuote(test.privateKey, test.quoteId, test.outputs) + if err != nil { + t.Fatalf("got unexpected error signing mint quote: %v", err) + } + + if !VerifyMintQuoteSignature(sig, test.quoteId, test.outputs, test.privateKey.PubKey()) { + t.Fatal("generated invalid signature on mint quote") + } + } +} + +func TestVerifyMintQuoteSignature(t *testing.T) { + tests := []struct { + quoteId string + outputs cashu.BlindedMessages + pubkey string + signature string + expected bool + }{ + { + quoteId: "9d745270-1405-46de-b5c5-e2762b4f5e00", + outputs: cashu.BlindedMessages{ + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79", + }, + }, + pubkey: "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", + signature: "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acbc0026a43c221fd773471dc30d6a32b04692e6837ddaccf0830a63128308e4ee0", + expected: true, + }, + { + quoteId: "9d745270-1405-46de-b5c5-e2762b4f5e00", + outputs: cashu.BlindedMessages{ + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "0342e5bcc77f5b2a3c2afb40bb591a1e27da83cddc968abdc0ec4904201a201834", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "032fd3c4dc49a2844a89998d5e9d5b0f0b00dde9310063acb8a92e2fdafa4126d4", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "033b6fde50b6a0dfe61ad148fff167ad9cf8308ded5f6f6b2fe000a036c464c311", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "02be5a55f03e5c0aaea77595d574bce92c6d57a2a0fb2b5955c0b87e4520e06b53", + }, + cashu.BlindedMessage{ + Amount: 1, + Id: "00456a94ab4e1c46", + B_: "02209fc2873f28521cbdde7f7b3bb1521002463f5979686fd156f23fe6a8aa2b79", + }, + }, + pubkey: "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac", + signature: "cb2b8e7ea69362dfe2a07093f2bbc319226db33db2ef686c940b5ec976bcbfc78df0cd35b3e998adf437b09ee2c950bd66dfe9eb64abd706e43ebc7c669c36c3", + expected: false, + }, + } + + for _, test := range tests { + sigBytes, _ := hex.DecodeString(test.signature) + signature, _ := schnorr.ParseSignature(sigBytes) + + pubkeyBytes, _ := hex.DecodeString(test.pubkey) + publickey, _ := secp256k1.ParsePubKey(pubkeyBytes) + + valid := VerifyMintQuoteSignature(signature, test.quoteId, test.outputs, publickey) + if valid != test.expected { + t.Fatalf("expected '%v' but got '%v'", test.expected, valid) + } + } + +} diff --git a/mint/mint.go b/mint/mint.go index df2af06..c4f0a70 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -18,6 +18,7 @@ import ( "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/chaincfg" "github.com/decred/dcrd/dcrec/secp256k1/v4" @@ -30,6 +31,7 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut11" "github.com/elnosh/gonuts/cashu/nuts/nut14" "github.com/elnosh/gonuts/cashu/nuts/nut17" + "github.com/elnosh/gonuts/cashu/nuts/nut20" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint/lightning" "github.com/elnosh/gonuts/mint/pubsub" @@ -269,6 +271,21 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.UnitErrCode) } + var publicKey *secp256k1.PublicKey + if len(mintQuoteRequest.Pubkey) > 0 { + hexPubkey, err := hex.DecodeString(mintQuoteRequest.Pubkey) + if err != nil { + errmsg := fmt.Sprintf("invalid public key '%v'", err) + return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.StandardErrCode) + } + + publicKey, err = secp256k1.ParsePubKey(hexPubkey) + if err != nil { + errmsg := fmt.Sprintf("invalid public key '%v'", err) + return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.StandardErrCode) + } + } + // check limits requestAmount := mintQuoteRequest.Amount if m.limits.MintingSettings.MaxAmount > 0 { @@ -307,6 +324,7 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques PaymentHash: invoice.PaymentHash, State: nut04.Unpaid, Expiry: uint64(time.Now().Add(time.Second * time.Duration(invoice.Expiry)).Unix()), + Pubkey: publicKey, } err = m.db.SaveMintQuote(mintQuote) @@ -407,11 +425,36 @@ func (m *Mint) MintTokens(mintTokensRequest nut04.PostMintBolt11Request) (cashu. errmsg := fmt.Sprintf("error getting blind signatures from db: %v", err) return cashu.BuildCashuError(errmsg, cashu.DBErrCode) } - if len(sigs) > 0 { return cashu.BlindedMessageAlreadySigned } + // verify signature on mint quote + if mintQuote.Pubkey != nil { + if len(mintTokensRequest.Signature) == 0 { + return cashu.MintQuoteInvalidSigErr + } + + sigBytes, err := hex.DecodeString(mintTokensRequest.Signature) + if err != nil { + return cashu.MintQuoteInvalidSigErr + } + signature, err := schnorr.ParseSignature(sigBytes) + if err != nil { + return cashu.MintQuoteInvalidSigErr + } + + if !nut20.VerifyMintQuoteSignature( + signature, + mintQuote.Id, + mintTokensRequest.Outputs, + mintQuote.Pubkey, + ) { + return cashu.MintQuoteInvalidSigErr + } + m.logDebugf("verified signature on mint quote") + } + blindedSignatures, err = m.signBlindedMessages(blindedMessages) if err != nil { return err @@ -1495,6 +1538,7 @@ func (m *Mint) SetMintInfo(mintInfo MintInfo) { }, }, }, + Nut20: nut06.Supported{Supported: true}, } if m.mppEnabled { diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index afd64f7..1bad5d1 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -12,11 +12,13 @@ import ( "os" "path/filepath" "reflect" + "strings" "sync" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" + "github.com/decred/dcrd/dcrec/secp256k1/v4" btcdocker "github.com/elnosh/btc-docker-test" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut04" @@ -26,6 +28,7 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut11" "github.com/elnosh/gonuts/cashu/nuts/nut12" "github.com/elnosh/gonuts/cashu/nuts/nut14" + "github.com/elnosh/gonuts/cashu/nuts/nut20" "github.com/elnosh/gonuts/crypto" "github.com/elnosh/gonuts/mint" "github.com/elnosh/gonuts/mint/storage" @@ -118,6 +121,19 @@ func TestRequestMintQuote(t *testing.T) { if cashuErr.Code != cashu.UnitErrCode { t.Fatalf("expected cashu error code '%v' but got '%v' instead", cashu.UnitErrCode, cashuErr.Code) } + + // test invalid pubkey + mintQuoteRequest = nut04.PostMintQuoteBolt11Request{ + Amount: mintAmount, + Unit: cashu.Sat.String(), + Pubkey: "invalidpubkey", + } + _, err = testMint.RequestMintQuote(mintQuoteRequest) + cashuErr, _ = err.(*cashu.Error) + invalidPubkeyErr := "invalid public key" + if !strings.Contains(cashuErr.Detail, invalidPubkeyErr) { + t.Fatalf("expected error '%v' but got '%v' instead", invalidPubkeyErr, cashuErr.Detail) + } } func TestMintQuoteState(t *testing.T) { @@ -180,7 +196,6 @@ func TestMintQuoteState(t *testing.T) { if quoteStateResponse.State != nut04.Issued { t.Fatalf("expected quote state '%s' but got '%s' instead", nut04.Issued, quoteStateResponse.State) } - } func TestMintTokens(t *testing.T) { @@ -266,6 +281,59 @@ func TestMintTokens(t *testing.T) { if !errors.Is(err, cashu.BlindedMessageAlreadySigned) { t.Fatalf("expected error '%v' but got '%v' instead", cashu.BlindedMessageAlreadySigned, err) } + + // test signature on mint quote + privateKey, _ := secp256k1.GeneratePrivateKey() + mintQuoteRequest = nut04.PostMintQuoteBolt11Request{ + Amount: mintAmount, + Unit: cashu.Sat.String(), + Pubkey: hex.EncodeToString(privateKey.PubKey().SerializeCompressed()), + } + mintQuoteResponse, err = testMint.RequestMintQuote(mintQuoteRequest) + if err != nil { + t.Fatalf("error requesting mint quote: %v", err) + } + blindedMessages, _, _, _ = testutils.CreateBlindedMessages(mintAmount, keyset) + + sendPaymentRequest = lnrpc.SendRequest{ + PaymentRequest: mintQuoteResponse.PaymentRequest, + } + response, _ = lnd2.Client.SendPaymentSync(ctx, &sendPaymentRequest) + if len(response.PaymentError) > 0 { + t.Fatalf("error paying invoice: %v", response.PaymentError) + } + + // test no signature for mint quote with pubkey + mintTokensRequest = nut04.PostMintBolt11Request{Quote: mintQuoteResponse.Id, Outputs: blindedMessages} + _, err = testMint.MintTokens(mintTokensRequest) + if !errors.Is(err, cashu.MintQuoteInvalidSigErr) { + t.Fatalf("expected error '%v' but got '%v' instead", cashu.MintQuoteInvalidSigErr, err) + } + + // test invalid signature on mint quote + incorrectKey, _ := secp256k1.GeneratePrivateKey() + sig, _ := nut20.SignMintQuote(incorrectKey, mintQuoteResponse.Id, blindedMessages) + mintTokensRequest = nut04.PostMintBolt11Request{ + Quote: mintQuoteResponse.Id, + Outputs: blindedMessages, + Signature: hex.EncodeToString(sig.Serialize()), + } + _, err = testMint.MintTokens(mintTokensRequest) + if !errors.Is(err, cashu.MintQuoteInvalidSigErr) { + t.Fatalf("expected error '%v' but got '%v' instead", cashu.MintQuoteInvalidSigErr, err) + } + + // test valid signature on mint quote + validSig, _ := nut20.SignMintQuote(privateKey, mintQuoteResponse.Id, blindedMessages) + mintTokensRequest = nut04.PostMintBolt11Request{ + Quote: mintQuoteResponse.Id, + Outputs: blindedMessages, + Signature: hex.EncodeToString(validSig.Serialize()), + } + _, err = testMint.MintTokens(mintTokensRequest) + if err != nil { + t.Fatalf("got unexpected error minting tokens: %v", err) + } } func TestSwap(t *testing.T) { diff --git a/mint/server.go b/mint/server.go index c343f2a..b08396b 100644 --- a/mint/server.go +++ b/mint/server.go @@ -2,6 +2,7 @@ package mint import ( "context" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -248,6 +249,10 @@ func (ms *MintServer) mintRequest(rw http.ResponseWriter, req *http.Request) { State: mintQuote.State, Expiry: mintQuote.Expiry, } + if mintQuote.Pubkey != nil { + mintQuoteResponse.Pubkey = hex.EncodeToString(mintQuote.Pubkey.SerializeCompressed()) + } + jsonRes, err := json.Marshal(&mintQuoteResponse) if err != nil { ms.writeErr(rw, req, cashu.StandardErr) @@ -289,6 +294,10 @@ func (ms *MintServer) mintQuoteState(rw http.ResponseWriter, req *http.Request) State: mintQuote.State, Expiry: mintQuote.Expiry, } + if mintQuote.Pubkey != nil { + mintQuoteStateResponse.Pubkey = hex.EncodeToString(mintQuote.Pubkey.SerializeCompressed()) + } + jsonRes, err := json.Marshal(&mintQuoteStateResponse) if err != nil { ms.writeErr(rw, req, cashu.StandardErr) diff --git a/mint/storage/sqlite/migrations/000009_mint_quote_pubkey.down.sql b/mint/storage/sqlite/migrations/000009_mint_quote_pubkey.down.sql new file mode 100644 index 0000000..3af5cee --- /dev/null +++ b/mint/storage/sqlite/migrations/000009_mint_quote_pubkey.down.sql @@ -0,0 +1 @@ +ALTER TABLE mint_quotes DROP COLUMN pubkey; diff --git a/mint/storage/sqlite/migrations/000009_mint_quote_pubkey.up.sql b/mint/storage/sqlite/migrations/000009_mint_quote_pubkey.up.sql new file mode 100644 index 0000000..b5ffc52 --- /dev/null +++ b/mint/storage/sqlite/migrations/000009_mint_quote_pubkey.up.sql @@ -0,0 +1 @@ +ALTER TABLE mint_quotes ADD COLUMN pubkey TEXT; diff --git a/mint/storage/sqlite/sqlite.go b/mint/storage/sqlite/sqlite.go index d8cd3cf..236843b 100644 --- a/mint/storage/sqlite/sqlite.go +++ b/mint/storage/sqlite/sqlite.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" @@ -400,15 +401,21 @@ func (sqlite *SQLiteDB) RemovePendingProofs(Ys []string) error { } func (sqlite *SQLiteDB) SaveMintQuote(mintQuote storage.MintQuote) error { + var pubkey string + if mintQuote.Pubkey != nil { + pubkey = hex.EncodeToString(mintQuote.Pubkey.SerializeCompressed()) + } + _, err := sqlite.db.Exec( - `INSERT INTO mint_quotes (id, payment_request, payment_hash, amount, state, expiry) - VALUES (?, ?, ?, ?, ?, ?)`, + `INSERT INTO mint_quotes (id, payment_request, payment_hash, amount, state, expiry, pubkey) + VALUES (?, ?, ?, ?, ?, ?, ?)`, mintQuote.Id, mintQuote.PaymentRequest, mintQuote.PaymentHash, mintQuote.Amount, mintQuote.State.String(), mintQuote.Expiry, + pubkey, ) return err @@ -419,6 +426,7 @@ func (sqlite *SQLiteDB) GetMintQuote(quoteId string) (storage.MintQuote, error) var mintQuote storage.MintQuote var state string + var pubkey sql.NullString err := row.Scan( &mintQuote.Id, @@ -427,12 +435,28 @@ func (sqlite *SQLiteDB) GetMintQuote(quoteId string) (storage.MintQuote, error) &mintQuote.Amount, &state, &mintQuote.Expiry, + &pubkey, ) if err != nil { return storage.MintQuote{}, err } mintQuote.State = nut04.StringToState(state) + if pubkey.Valid && len(pubkey.String) > 0 { + // these should not error because validation is done before saving with public key + // if there is an error, something bad happened + hexPubkey, err := hex.DecodeString(pubkey.String) + if err != nil { + return storage.MintQuote{}, fmt.Errorf("invalid public key in db: %v", err) + } + + publicKey, err := secp256k1.ParsePubKey(hexPubkey) + if err != nil { + return storage.MintQuote{}, fmt.Errorf("invalid public key in db: %v", err) + } + mintQuote.Pubkey = publicKey + } + return mintQuote, nil } @@ -441,6 +465,7 @@ func (sqlite *SQLiteDB) GetMintQuoteByPaymentHash(paymentHash string) (storage.M var mintQuote storage.MintQuote var state string + var pubkey sql.NullString err := row.Scan( &mintQuote.Id, @@ -449,12 +474,28 @@ func (sqlite *SQLiteDB) GetMintQuoteByPaymentHash(paymentHash string) (storage.M &mintQuote.Amount, &state, &mintQuote.Expiry, + &pubkey, ) if err != nil { return storage.MintQuote{}, err } mintQuote.State = nut04.StringToState(state) + if pubkey.Valid && len(pubkey.String) > 0 { + // these should not error because validation is done before saving with public key + // if there is an error, something bad happened + hexPubkey, err := hex.DecodeString(pubkey.String) + if err != nil { + return storage.MintQuote{}, fmt.Errorf("invalid public key in db: %v", err) + } + + publicKey, err := secp256k1.ParsePubKey(hexPubkey) + if err != nil { + return storage.MintQuote{}, fmt.Errorf("invalid public key in db: %v", err) + } + mintQuote.Pubkey = publicKey + } + return mintQuote, nil } diff --git a/mint/storage/storage.go b/mint/storage/storage.go index 5e2f97f..f64a93d 100644 --- a/mint/storage/storage.go +++ b/mint/storage/storage.go @@ -1,6 +1,7 @@ package storage import ( + "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" @@ -68,6 +69,7 @@ type MintQuote struct { PaymentHash string State nut04.State Expiry uint64 + Pubkey *secp256k1.PublicKey } type MeltQuote struct {