diff --git a/cmd/mint/mint.go b/cmd/mint/mint.go index 468b5df..3af0a9a 100644 --- a/cmd/mint/mint.go +++ b/cmd/mint/mint.go @@ -12,7 +12,7 @@ func main() { if err != nil { log.Fatal("error loading .env file") } - mintConfig := mint.GetConfig() + mintConfig := mint.ConfigFromEnv() mintServer, err := mint.SetupMintServer(mintConfig) if err != nil { diff --git a/mint/config.go b/mint/config.go index c8bab11..8d1f3c5 100644 --- a/mint/config.go +++ b/mint/config.go @@ -1,15 +1,12 @@ package mint import ( - "encoding/hex" "encoding/json" "fmt" "log" "os" "strconv" - "github.com/btcsuite/btcd/btcutil/hdkeychain" - "github.com/btcsuite/btcd/chaincfg" "github.com/elnosh/gonuts/cashu/nuts/nut06" ) @@ -19,6 +16,7 @@ type Config struct { DBPath string DBMigrationPath string InputFeePpk uint + MintInfo nut06.MintInfo Limits MintLimits } @@ -38,7 +36,7 @@ type MintLimits struct { MeltingSettings MeltMethodSettings } -func GetConfig() Config { +func ConfigFromEnv() Config { var inputFeePpk uint = 0 if inputFeeEnv, ok := os.LookupEnv("INPUT_FEE_PPK"); ok { fee, err := strconv.ParseUint(inputFeeEnv, 10, 16) @@ -77,6 +75,7 @@ func GetConfig() Config { } mintLimits.MeltingSettings = MeltMethodSettings{MaxAmount: maxMelt} } + mintInfo, _ := MintInfoEnv() return Config{ DerivationPathIdx: uint32(derivationPathIdx), @@ -84,46 +83,27 @@ func GetConfig() Config { DBPath: os.Getenv("MINT_DB_PATH"), DBMigrationPath: "../../mint/storage/sqlite/migrations", InputFeePpk: inputFeePpk, + MintInfo: mintInfo, Limits: mintLimits, } } -// getMintInfo returns information about the mint as -// defined in NUT-06: https://github.com/cashubtc/nuts/blob/main/06.md -func (m *Mint) getMintInfo() (*nut06.MintInfo, error) { +func MintInfoEnv() (nut06.MintInfo, error) { mintInfo := nut06.MintInfo{ Name: os.Getenv("MINT_NAME"), - Version: "gonuts/0.0.1", Description: os.Getenv("MINT_DESCRIPTION"), } mintInfo.LongDescription = os.Getenv("MINT_DESCRIPTION_LONG") mintInfo.Motd = os.Getenv("MINT_MOTD") - seed, err := m.db.GetSeed() - if err != nil { - return nil, err - } - - master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) - if err != nil { - return nil, err - } - - publicKey, err := master.ECPubKey() - if err != nil { - return nil, err - } - - mintInfo.Pubkey = hex.EncodeToString(publicKey.SerializeCompressed()) - contact := os.Getenv("MINT_CONTACT_INFO") var mintContactInfo []nut06.ContactInfo if len(contact) > 0 { var infoArr [][]string err := json.Unmarshal([]byte(contact), &infoArr) if err != nil { - return nil, fmt.Errorf("error parsing contact info: %v", err) + return nut06.MintInfo{}, fmt.Errorf("error parsing contact info: %v", err) } for _, info := range infoArr { @@ -132,38 +112,5 @@ func (m *Mint) getMintInfo() (*nut06.MintInfo, error) { } } mintInfo.Contact = mintContactInfo - - nuts := nut06.NutsMap{ - 4: nut06.NutSetting{ - Methods: []nut06.MethodSetting{ - { - Method: "bolt11", - Unit: "sat", - MinAmount: m.Limits.MintingSettings.MinAmount, - MaxAmount: m.Limits.MintingSettings.MaxAmount, - }, - }, - Disabled: false, - }, - 5: nut06.NutSetting{ - Methods: []nut06.MethodSetting{ - { - Method: "bolt11", - Unit: "sat", - MinAmount: m.Limits.MeltingSettings.MinAmount, - MaxAmount: m.Limits.MeltingSettings.MaxAmount, - }, - }, - Disabled: false, - }, - 7: map[string]bool{"supported": false}, - 8: map[string]bool{"supported": false}, - 9: map[string]bool{"supported": false}, - 10: map[string]bool{"supported": false}, - 11: map[string]bool{"supported": false}, - 12: map[string]bool{"supported": false}, - } - - mintInfo.Nuts = nuts - return &mintInfo, nil + return mintInfo, nil } diff --git a/mint/mint.go b/mint/mint.go index 65e653b..2724594 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -35,14 +35,14 @@ type Mint struct { db storage.MintDB // active keysets - ActiveKeysets map[string]crypto.MintKeyset + activeKeysets map[string]crypto.MintKeyset // map of all keysets (both active and inactive) - Keysets map[string]crypto.MintKeyset + keysets map[string]crypto.MintKeyset - LightningClient lightning.Client - MintInfo *nut06.MintInfo - Limits MintLimits + lightningClient lightning.Client + mintInfo nut06.MintInfo + limits MintLimits } func LoadMint(config Config) (*Mint, error) { @@ -87,8 +87,8 @@ func LoadMint(config Config) (*Mint, error) { mint := &Mint{ db: db, - ActiveKeysets: map[string]crypto.MintKeyset{activeKeyset.Id: *activeKeyset}, - Limits: config.Limits, + activeKeysets: map[string]crypto.MintKeyset{activeKeyset.Id: *activeKeyset}, + limits: config.Limits, } dbKeysets, err := mint.db.GetKeysets() @@ -136,19 +136,16 @@ func LoadMint(config Config) (*Mint, error) { } } - mint.Keysets = mintKeysets - mint.Keysets[activeKeyset.Id] = *activeKeyset - mint.LightningClient = lightning.NewLightningClient() - mint.MintInfo, err = mint.getMintInfo() - if err != nil { - return nil, err - } + mint.keysets = mintKeysets + mint.keysets[activeKeyset.Id] = *activeKeyset + mint.lightningClient = lightning.NewLightningClient() + mint.mintInfo = config.MintInfo - for _, keyset := range mint.Keysets { + for _, keyset := range mint.keysets { if keyset.Id != activeKeyset.Id && keyset.Active { keyset.Active = false mint.db.UpdateKeysetActive(keyset.Id, false) - mint.Keysets[keyset.Id] = keyset + mint.keysets[keyset.Id] = keyset } } @@ -186,17 +183,17 @@ func (m *Mint) RequestMintQuote(method string, amount uint64, unit string) (stor } // check limits - if m.Limits.MintingSettings.MaxAmount > 0 { - if amount > m.Limits.MintingSettings.MaxAmount { + if m.limits.MintingSettings.MaxAmount > 0 { + if amount > m.limits.MintingSettings.MaxAmount { return storage.MintQuote{}, cashu.MintAmountExceededErr } } - if m.Limits.MaxBalance > 0 { + if m.limits.MaxBalance > 0 { balance, err := m.db.GetBalance() if err != nil { return storage.MintQuote{}, err } - if balance+amount > m.Limits.MaxBalance { + if balance+amount > m.limits.MaxBalance { return storage.MintQuote{}, cashu.MintingDisabled } } @@ -243,7 +240,7 @@ func (m *Mint) GetMintQuoteState(method, quoteId string) (storage.MintQuote, err } // check if the invoice has been paid - status, err := m.LightningClient.InvoiceStatus(mintQuote.PaymentHash) + status, err := m.lightningClient.InvoiceStatus(mintQuote.PaymentHash) if err != nil { msg := fmt.Sprintf("error getting status of payment request: %v", err) return storage.MintQuote{}, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) @@ -274,7 +271,7 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag } var blindedSignatures cashu.BlindedSignatures - status, err := m.LightningClient.InvoiceStatus(mintQuote.PaymentHash) + status, err := m.lightningClient.InvoiceStatus(mintQuote.PaymentHash) if err != nil { msg := fmt.Sprintf("error getting status of payment request: %v", err) return nil, cashu.BuildCashuError(msg, cashu.LightningBackendErrCode) @@ -395,8 +392,8 @@ func (m *Mint) RequestMeltQuote(method, request, unit string) (storage.MeltQuote satAmount := uint64(bolt11.MSatoshi) / 1000 // check melt limit - if m.Limits.MeltingSettings.MaxAmount > 0 { - if satAmount > m.Limits.MeltingSettings.MaxAmount { + if m.limits.MeltingSettings.MaxAmount > 0 { + if satAmount > m.limits.MeltingSettings.MaxAmount { return storage.MeltQuote{}, cashu.MeltAmountExceededErr } } @@ -407,7 +404,7 @@ func (m *Mint) RequestMeltQuote(method, request, unit string) (storage.MeltQuote } // Fee reserve that is required by the mint - fee := m.LightningClient.FeeReserve(satAmount) + fee := m.lightningClient.FeeReserve(satAmount) expiry := uint64(time.Now().Add(time.Minute * QuoteExpiryMins).Unix()) meltQuote := storage.MeltQuote{ @@ -483,7 +480,7 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (storage. // if proofs are valid, ask the lightning backend // to make the payment - preimage, err := m.LightningClient.SendPayment(meltQuote.InvoiceRequest, meltQuote.Amount) + preimage, err := m.lightningClient.SendPayment(meltQuote.InvoiceRequest, meltQuote.Amount) if err != nil { return storage.MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.LightningBackendErrCode) } @@ -533,7 +530,7 @@ func (m *Mint) verifyProofs(proofs cashu.Proofs, Ys []string) error { // check that id in the proof matches id of any // of the mint's keyset var k *secp256k1.PrivateKey - if keyset, ok := m.Keysets[proof.Id]; !ok { + if keyset, ok := m.keysets[proof.Id]; !ok { return cashu.UnknownKeysetErr } else { if key, ok := keyset.Keys[proof.Amount]; ok { @@ -566,11 +563,11 @@ func (m *Mint) signBlindedMessages(blindedMessages cashu.BlindedMessages) (cashu blindedSignatures := make(cashu.BlindedSignatures, len(blindedMessages)) for i, msg := range blindedMessages { - if _, ok := m.Keysets[msg.Id]; !ok { + if _, ok := m.keysets[msg.Id]; !ok { return nil, cashu.UnknownKeysetErr } var k *secp256k1.PrivateKey - keyset, ok := m.ActiveKeysets[msg.Id] + keyset, ok := m.activeKeysets[msg.Id] if !ok { return nil, cashu.InactiveKeysetSignatureRequest } else { @@ -605,7 +602,7 @@ func (m *Mint) signBlindedMessages(blindedMessages cashu.BlindedMessages) (cashu // requestInvoices requests an invoice from the Lightning backend // for the given amount func (m *Mint) requestInvoice(amount uint64) (*lightning.Invoice, error) { - invoice, err := m.LightningClient.CreateInvoice(amount) + invoice, err := m.lightningClient.CreateInvoice(amount) if err != nil { return nil, err } @@ -617,7 +614,81 @@ func (m *Mint) TransactionFees(inputs cashu.Proofs) uint { for _, proof := range inputs { // note: not checking that proof id is from valid keyset // because already doing that in call to verifyProofs - fees += m.Keysets[proof.Id].InputFeePpk + fees += m.keysets[proof.Id].InputFeePpk } return (fees + 999) / 1000 } + +func (m *Mint) GetActiveKeyset() crypto.MintKeyset { + var keyset crypto.MintKeyset + for _, k := range m.activeKeysets { + keyset = k + break + } + return keyset +} + +func (m *Mint) RetrieveMintInfo() (nut06.MintInfo, error) { + m.mintInfo.Version = "gonuts/0.0.1" + + seed, err := m.db.GetSeed() + if err != nil { + return nut06.MintInfo{}, err + } + master, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams) + if err != nil { + return nut06.MintInfo{}, err + } + publicKey, err := master.ECPubKey() + if err != nil { + return nut06.MintInfo{}, err + } + m.mintInfo.Pubkey = hex.EncodeToString(publicKey.SerializeCompressed()) + + mintingDisabled := false + mintBalance, err := m.db.GetBalance() + if err != nil { + msg := fmt.Sprintf("error getting mint balance: %v", err) + return nut06.MintInfo{}, cashu.BuildCashuError(msg, cashu.DBErrCode) + } + + if m.limits.MaxBalance > 0 { + if mintBalance >= m.limits.MaxBalance { + mintingDisabled = true + } + } + + nuts := nut06.NutsMap{ + 4: nut06.NutSetting{ + Methods: []nut06.MethodSetting{ + { + Method: BOLT11_METHOD, + Unit: SAT_UNIT, + MinAmount: m.limits.MintingSettings.MinAmount, + MaxAmount: m.limits.MintingSettings.MaxAmount, + }, + }, + Disabled: mintingDisabled, + }, + 5: nut06.NutSetting{ + Methods: []nut06.MethodSetting{ + { + Method: BOLT11_METHOD, + Unit: SAT_UNIT, + MinAmount: m.limits.MeltingSettings.MinAmount, + MaxAmount: m.limits.MeltingSettings.MaxAmount, + }, + }, + Disabled: false, + }, + 7: map[string]bool{"supported": false}, + 8: map[string]bool{"supported": false}, + 9: map[string]bool{"supported": false}, + 10: map[string]bool{"supported": false}, + 11: map[string]bool{"supported": false}, + 12: map[string]bool{"supported": false}, + } + + m.mintInfo.Nuts = nuts + return m.mintInfo, nil +} diff --git a/mint/mint_integration_test.go b/mint/mint_integration_test.go index e50c0ad..18b93b7 100644 --- a/mint/mint_integration_test.go +++ b/mint/mint_integration_test.go @@ -121,11 +121,7 @@ func TestMintQuoteState(t *testing.T) { t.Fatalf("error requesting mint quote: %v", err) } - var keyset crypto.MintKeyset - for _, k := range testMint.ActiveKeysets { - keyset = k - break - } + keyset := testMint.GetActiveKeyset() // test invalid method _, err = testMint.GetMintQuoteState("strike", mintQuoteResponse.Id) @@ -192,11 +188,7 @@ func TestMintTokens(t *testing.T) { t.Fatalf("error requesting mint quote: %v", err) } - var keyset crypto.MintKeyset - for _, k := range testMint.ActiveKeysets { - keyset = k - break - } + keyset := testMint.GetActiveKeyset() blindedMessages, _, _, err := testutils.CreateBlindedMessages(mintAmount, keyset) @@ -255,11 +247,7 @@ func TestSwap(t *testing.T) { t.Fatalf("error generating valid proofs: %v", err) } - var keyset crypto.MintKeyset - for _, k := range testMint.ActiveKeysets { - keyset = k - break - } + keyset := testMint.GetActiveKeyset() newBlindedMessages, _, _, err := testutils.CreateBlindedMessages(amount, keyset) overBlindedMessages, _, _, err := testutils.CreateBlindedMessages(amount+200, keyset) @@ -308,10 +296,7 @@ func TestSwap(t *testing.T) { t.Fatalf("error generating valid proofs: %v", err) } - for _, k := range mintFees.ActiveKeysets { - keyset = k - break - } + keyset = mintFees.GetActiveKeyset() fees := mintFees.TransactionFees(proofs) invalidAmtblindedMessages, _, _, err := testutils.CreateBlindedMessages(amount, keyset) @@ -573,11 +558,7 @@ func TestMintLimits(t *testing.T) { os.RemoveAll(limitsMintPath) }() - var keyset crypto.MintKeyset - for _, k := range limitsMint.ActiveKeysets { - keyset = k - break - } + keyset := limitsMint.GetActiveKeyset() // test above mint max amount var mintAmount uint64 = 20000 diff --git a/mint/server.go b/mint/server.go index ac6ba6f..892e5a9 100644 --- a/mint/server.go +++ b/mint/server.go @@ -154,7 +154,7 @@ func (ms *MintServer) writeErr(rw http.ResponseWriter, req *http.Request, errRes } func (ms *MintServer) getActiveKeysets(rw http.ResponseWriter, req *http.Request) { - getKeysResponse := buildKeysResponse(ms.mint.ActiveKeysets) + getKeysResponse := buildKeysResponse(ms.mint.activeKeysets) jsonRes, err := json.Marshal(getKeysResponse) if err != nil { ms.writeErr(rw, req, cashu.StandardErr) @@ -179,7 +179,7 @@ func (ms *MintServer) getKeysetById(rw http.ResponseWriter, req *http.Request) { vars := mux.Vars(req) id := vars["id"] - ks, ok := ms.mint.Keysets[id] + ks, ok := ms.mint.keysets[id] if !ok { ms.writeErr(rw, req, cashu.UnknownKeysetErr) return @@ -469,7 +469,12 @@ func (ms *MintServer) meltTokens(rw http.ResponseWriter, req *http.Request) { } func (ms *MintServer) mintInfo(rw http.ResponseWriter, req *http.Request) { - jsonRes, err := json.Marshal(ms.mint.MintInfo) + mintInfo, err := ms.mint.RetrieveMintInfo() + if err != nil { + ms.writeErr(rw, req, cashu.StandardErr) + return + } + jsonRes, err := json.Marshal(mintInfo) if err != nil { ms.writeErr(rw, req, cashu.StandardErr) return @@ -492,7 +497,7 @@ func buildKeysResponse(keysets map[string]crypto.MintKeyset) nut01.GetKeysRespon func (ms *MintServer) buildAllKeysetsResponse() nut02.GetKeysetsResponse { keysetsResponse := nut02.GetKeysetsResponse{} - for _, keyset := range ms.mint.Keysets { + for _, keyset := range ms.mint.keysets { keysetRes := nut02.Keyset{ Id: keyset.Id, Unit: keyset.Unit, diff --git a/testutils/utils.go b/testutils/utils.go index 4056ec5..a81bf64 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -341,11 +341,7 @@ func GetValidProofsForAmount(amount uint64, mint *mint.Mint, payer *btcdocker.Ln return nil, fmt.Errorf("error requesting mint quote: %v", err) } - var keyset crypto.MintKeyset - for _, k := range mint.ActiveKeysets { - keyset = k - break - } + keyset := mint.GetActiveKeyset() blindedMessages, secrets, rs, err := CreateBlindedMessages(amount, keyset) if err != nil {