From 8b1a7c543e42be302f1c782c235012551ef6a84c Mon Sep 17 00:00:00 2001 From: elnosh Date: Mon, 15 Apr 2024 14:32:54 -0500 Subject: [PATCH] comments for mint and wallet packages --- mint/config.go | 2 + mint/lightning/lightning.go | 1 + mint/mint.go | 74 ++++++++++++++++++++++++------------- wallet/wallet.go | 43 +++++++++++++++++++-- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/mint/config.go b/mint/config.go index 1d61a3e..c81e78c 100644 --- a/mint/config.go +++ b/mint/config.go @@ -22,6 +22,8 @@ func GetConfig() Config { } } +// getMintInfo returns information about the mint as +// defined in NUT-06: https://github.com/cashubtc/nuts/blob/main/06.md func getMintInfo() (*nut06.MintInfo, error) { mintInfo := nut06.MintInfo{ Name: os.Getenv("MINT_NAME"), diff --git a/mint/lightning/lightning.go b/mint/lightning/lightning.go index 0292405..a63747c 100644 --- a/mint/lightning/lightning.go +++ b/mint/lightning/lightning.go @@ -9,6 +9,7 @@ const ( LND = "Lnd" ) +// Client interface to interact with a Lightning backend type Client interface { CreateInvoice(amount uint64) (Invoice, error) InvoiceSettled(hash string) bool diff --git a/mint/mint.go b/mint/mint.go index d5a43be..c8f718d 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -66,6 +66,8 @@ func LoadMint(config Config) (*Mint, error) { return mint, nil } +// mintPath returns the mint's path +// at $HOME/.gonuts/mint func mintPath() string { homedir, err := os.UserHomeDir() if err != nil { @@ -80,25 +82,21 @@ func mintPath() string { return path } -func (m *Mint) KeysetList() []string { - keysetIds := make([]string, len(m.Keysets)) - - i := 0 - for k := range m.Keysets { - keysetIds[i] = k - i++ - } - return keysetIds -} - +// RequestMintQuote will process a request to mint tokens +// and returns a mint quote response or an error. +// The request to mint a token is explained in +// NUT-04 here: https://github.com/cashubtc/nuts/blob/main/04.md. func (m *Mint) RequestMintQuote(method string, amount uint64, unit string) (nut04.PostMintQuoteBolt11Response, error) { + // only support bolt11 if method != "bolt11" { return nut04.PostMintQuoteBolt11Response{}, cashu.PaymentMethodNotSupportedErr } + // only support sat unit if unit != "sat" { return nut04.PostMintQuoteBolt11Response{}, cashu.UnitNotSupportedErr } + // get an invoice from the lightning backend invoice, err := m.requestInvoice(amount) if err != nil { return nut04.PostMintQuoteBolt11Response{}, err @@ -119,6 +117,8 @@ func (m *Mint) RequestMintQuote(method string, amount uint64, unit string) (nut0 return reqMintQuoteResponse, nil } +// GetMintQuoteState returns the state of a mint quote. +// Used to check whether a mint quote has been paid. func (m *Mint) GetMintQuoteState(method, quoteId string) (nut04.PostMintQuoteBolt11Response, error) { if method != "bolt11" { return nut04.PostMintQuoteBolt11Response{}, cashu.PaymentMethodNotSupportedErr @@ -129,6 +129,7 @@ func (m *Mint) GetMintQuoteState(method, quoteId string) (nut04.PostMintQuoteBol return nut04.PostMintQuoteBolt11Response{}, cashu.InvoiceNotExistErr } + // check if the invoice has been paid settled := m.LightningClient.InvoiceSettled(invoice.PaymentHash) if settled != invoice.Settled { invoice.Settled = settled @@ -140,7 +141,8 @@ func (m *Mint) GetMintQuoteState(method, quoteId string) (nut04.PostMintQuoteBol return quoteState, nil } -// id - quote id to lookup invoice +// MintTokens verifies whether the mint quote with id has been paid and proceeds to +// sign the blindedMessages and return the BlindedSignatures if it was paid. func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessages) (cashu.BlindedSignatures, error) { if method != "bolt11" { return nil, cashu.PaymentMethodNotSupportedErr @@ -164,6 +166,8 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag totalAmount += message.Amount } + // verify that amount from invoice is less than the amount + // from the blinded messages if totalAmount > invoice.Amount { return nil, cashu.OutputsOverInvoiceErr } @@ -174,6 +178,7 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag return nil, cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) } + // mark invoice as redeemed after signing the blinded messages invoice.Settled = true invoice.Redeemed = true m.db.SaveInvoice(*invoice) @@ -184,13 +189,14 @@ func (m *Mint) MintTokens(method, id string, blindedMessages cashu.BlindedMessag return blindedSignatures, nil } +// Swap will process a request to swap tokens. +// A swap requires a set of valid proofs and blinded messages. +// If valid, the mint will sign the blindedMessages and invalidate +// the proofs that were used as input. +// It returns the BlindedSignatures. func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) (cashu.BlindedSignatures, error) { - var proofsAmount uint64 = 0 var blindedMessagesAmount uint64 = 0 - - for _, proof := range proofs { - proofsAmount += proof.Amount - } + proofsAmount := proofs.Amount() for _, msg := range blindedMessages { blindedMessagesAmount += msg.Amount @@ -205,7 +211,8 @@ func (m *Mint) Swap(proofs cashu.Proofs, blindedMessages cashu.BlindedMessages) return nil, err } - // if verification complete, sign blinded messages and add used proofs to db + // if verification complete, sign blinded messages and invalidate used proofs + // by adding them to the db blindedSignatures, err := m.signBlindedMessages(blindedMessages) if err != nil { cashuErr := cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) @@ -229,6 +236,8 @@ type MeltQuote struct { Preimage string } +// MeltRequest will process a request to melt tokens and return a MeltQuote. +// A melt is requested by a wallet to request the mint to pay an invoice. func (m *Mint) MeltRequest(method, request, unit string) (MeltQuote, error) { if method != "bolt11" { return MeltQuote{}, cashu.PaymentMethodNotSupportedErr @@ -237,14 +246,15 @@ func (m *Mint) MeltRequest(method, request, unit string) (MeltQuote, error) { return MeltQuote{}, cashu.UnitNotSupportedErr } + // generate random id for melt quote randomBytes := make([]byte, 32) _, err := rand.Read(randomBytes) if err != nil { return MeltQuote{}, fmt.Errorf("melt request error: %v", err) } - hash := sha256.Sum256(randomBytes) + // Fee reserved that is required by the mint amount, fee, err := m.LightningClient.FeeReserve(request) if err != nil { return MeltQuote{}, fmt.Errorf("error getting fee: %v", err) @@ -264,6 +274,8 @@ func (m *Mint) MeltRequest(method, request, unit string) (MeltQuote, error) { return meltQuote, nil } +// GetMeltQuoteState returns the state of a melt quote. +// Used to check whether a melt quote has been paid. func (m *Mint) GetMeltQuoteState(method, quoteId string) (MeltQuote, error) { if method != "bolt11" { return MeltQuote{}, cashu.PaymentMethodNotSupportedErr @@ -277,6 +289,8 @@ func (m *Mint) GetMeltQuoteState(method, quoteId string) (MeltQuote, error) { return *meltQuote, nil } +// MeltTokens verifies whether proofs provided are valid +// and proceeds to attempt payment. func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (MeltQuote, error) { if method != "bolt11" { return MeltQuote{}, cashu.PaymentMethodNotSupportedErr @@ -292,22 +306,24 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (MeltQuot return MeltQuote{}, cashu.BuildCashuError(err.Error(), cashu.StandardErrCode) } - var inputsAmount uint64 = 0 - for _, input := range proofs { - inputsAmount += input.Amount - } + proofsAmount := proofs.Amount() - if inputsAmount < meltQuote.Amount+meltQuote.FeeReserve { + // checks if amount in proofs is enough + if proofsAmount < meltQuote.Amount+meltQuote.FeeReserve { return MeltQuote{}, cashu.InsufficientProofsAmount } + // if proofs are valid, ask the lightning backend + // to make the payment preimage, err := m.LightningClient.SendPayment(meltQuote.InvoiceRequest) if err != nil { return *meltQuote, nil } + + // if payment succeeded, mark melt quote as paid + // and invalidate proofs meltQuote.Paid = true meltQuote.Preimage = preimage - for _, proof := range proofs { m.db.SaveProof(proof) } @@ -317,11 +333,14 @@ func (m *Mint) MeltTokens(method, quoteId string, proofs cashu.Proofs) (MeltQuot func (m *Mint) VerifyProofs(proofs cashu.Proofs) (bool, error) { for _, proof := range proofs { + // if proof is already in db, it means it was already used dbProof := m.db.GetProof(proof.Secret) if dbProof != nil { return false, cashu.ProofAlreadyUsedErr } + // 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 { return false, cashu.InvalidKeysetProof @@ -350,6 +369,8 @@ func (m *Mint) VerifyProofs(proofs cashu.Proofs) (bool, error) { return true, nil } +// signBlindedMessages will sign the blindedMessages and +// return the blindedSignatures func (m *Mint) signBlindedMessages(blindedMessages cashu.BlindedMessages) (cashu.BlindedSignatures, error) { blindedSignatures := make(cashu.BlindedSignatures, len(blindedMessages)) @@ -387,7 +408,8 @@ func (m *Mint) signBlindedMessages(blindedMessages cashu.BlindedMessages) (cashu return blindedSignatures, nil } -// creates lightning invoice +// 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) if err != nil { diff --git a/wallet/wallet.go b/wallet/wallet.go index 431fc8a..58d4649 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -54,6 +54,7 @@ func mintInfo(mintURL string) (*walletMint, error) { return &walletMint{mintURL, activeKeysets, inactiveKeysets}, nil } +// addMint adds the mint to the list of mints trusted by the wallet func (w *Wallet) addMint(mint string) (*walletMint, error) { url, err := url.Parse(mint) if err != nil { @@ -169,10 +170,13 @@ func GetMintInactiveKeysets(mintURL string) (map[string]crypto.Keyset, error) { return inactiveKeysets, nil } +// GetBalance returns the total balance aggregated from all proofs func (w *Wallet) GetBalance() uint64 { return w.proofs.Amount() } +// GetBalanceByMints returns a map of string mint +// and a uint64 that represents the balance for that mint func (w *Wallet) GetBalanceByMints() map[string]uint64 { mintsBalances := make(map[string]uint64) @@ -194,6 +198,8 @@ func (w *Wallet) GetBalanceByMints() map[string]uint64 { return mintsBalances } +// RequestMint requests a mint quote to the wallet's current mint +// for the specified amount func (w *Wallet) RequestMint(amount uint64) (*nut04.PostMintQuoteBolt11Response, error) { mintRequest := nut04.PostMintQuoteBolt11Request{Amount: amount, Unit: "sat"} mintResponse, err := PostMintQuoteBolt11(w.currentMint.mintURL, mintRequest) @@ -222,6 +228,7 @@ func (w *Wallet) RequestMint(amount uint64) (*nut04.PostMintQuoteBolt11Response, return mintResponse, nil } +// CheckQuotePaid reports whether the mint quote has been paid func (w *Wallet) CheckQuotePaid(quoteId string) bool { mintQuote, err := GetMintQuoteState(w.currentMint.mintURL, quoteId) if err != nil { @@ -231,6 +238,11 @@ func (w *Wallet) CheckQuotePaid(quoteId string) bool { return mintQuote.Paid } +// MintTokens will check whether if the mint quote has been paid. +// If yes, it will create blinded messages that will send to the mint +// to get the blinded signatures. +// If successful, it will unblind the signatures to generate proofs +// and store the proofs in the db. func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { mintQuote, err := GetMintQuoteState(w.currentMint.mintURL, quoteId) if err != nil { @@ -248,12 +260,14 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { return nil, errors.New("invoice not found") } + // create blinded messages activeKeyset := w.GetActiveSatKeyset() blindedMessages, secrets, rs, err := w.CreateBlindedMessages(invoice.Amount, activeKeyset) if err != nil { return nil, fmt.Errorf("error creating blinded messages: %v", err) } + // request mint to sign the blinded messages postMintRequest := nut04.PostMintBolt11Request{Quote: quoteId, Outputs: blindedMessages} mintResponse, err := PostMintBolt11(w.currentMint.mintURL, postMintRequest) if err != nil { @@ -266,6 +280,7 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { return nil, fmt.Errorf("error constructing proofs: %v", err) } + // mark invoice as redeemed invoice.Settled = true invoice.Redeemed = true err = w.db.SaveInvoice(*invoice) @@ -282,6 +297,7 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { return proofs, nil } +// Send will return a cashu token with proofs for the given amount func (w *Wallet) Send(amount uint64, mintURL string) (*cashu.Token, error) { proofsToSend, err := w.getProofsForAmount(amount, mintURL) if err != nil { @@ -293,7 +309,7 @@ func (w *Wallet) Send(amount uint64, mintURL string) (*cashu.Token, error) { } // Receives Cashu token. If swap is true, it will swap the funds to the configured default mint. -// If false, it will add the proofs from the trusted mint. +// If false, it will add the proofs from the mint and add that mint to the list of trusted mints. func (w *Wallet) Receive(token cashu.Token, swap bool) (uint64, error) { if swap { trustedMintProofs, err := w.swapToTrusted(token) @@ -307,8 +323,8 @@ func (w *Wallet) Receive(token cashu.Token, swap bool) (uint64, error) { proofsToSwap = append(proofsToSwap, tokenProof.Proofs...) } + // add mint to list of trusted mints tokenMintURL := token.Token[0].Mint - mint, err := w.addMint(tokenMintURL) if err != nil { return 0, err @@ -321,17 +337,20 @@ func (w *Wallet) Receive(token cashu.Token, swap bool) (uint64, error) { break } + // create blinded messages outputs, secrets, rs, err := w.CreateBlindedMessages(token.TotalAmount(), activeSatKeyset) if err != nil { return 0, fmt.Errorf("CreateBlindedMessages: %v", err) } + // make swap request to mint swapRequest := nut03.PostSwapRequest{Inputs: proofsToSwap, Outputs: outputs} swapResponse, err := PostSwap(tokenMintURL, swapRequest) if err != nil { return 0, err } + // unblind signatures to get proofs and save them to db proofs, err := w.ConstructProofs(swapResponse.Signatures, secrets, rs, &activeSatKeyset) if err != nil { return 0, fmt.Errorf("wallet.ConstructProofs: %v", err) @@ -342,6 +361,8 @@ func (w *Wallet) Receive(token cashu.Token, swap bool) (uint64, error) { } } +// swapToTrusted will swap the proofs from mint in the token +// to the wallet's configured default mint func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { invoicePct := 0.99 tokenAmount := token.TotalAmount() @@ -358,17 +379,23 @@ func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { var err error for { + // request a mint quote from the configured default mint + // this will generate an invoice from the trusted mint mintResponse, err = w.RequestMint(uint64(amount)) if err != nil { return nil, fmt.Errorf("error requesting mint: %v", err) } + // request melt quote from untrusted mint which will + // request mint to pay invoice generated from trusted mint in previous mint request meltRequest := nut05.PostMeltQuoteBolt11Request{Request: mintResponse.Request, Unit: "sat"} meltQuoteResponse, err = PostMeltQuoteBolt11(tokenMintURL, meltRequest) if err != nil { return nil, fmt.Errorf("error with melt request: %v", err) } + // if amount in token is less than amount asked from mint in melt request, + // lower the amount for mint request if meltQuoteResponse.Amount+meltQuoteResponse.FeeReserve > tokenAmount { invoicePct -= 0.01 amount *= invoicePct @@ -377,12 +404,15 @@ func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { } } + // request untrusted mint to pay invoice generated from trusted mint meltBolt11Request := nut05.PostMeltBolt11Request{Quote: meltQuoteResponse.Quote, Inputs: proofsToSwap} meltBolt11Response, err := PostMeltBolt11(tokenMintURL, meltBolt11Request) if err != nil { return nil, fmt.Errorf("error melting token: %v", err) } + // if melt request was successful and untrusted mint paid the invoice, + // make mint request to trusted mint to get valid proofs if meltBolt11Response.Paid { proofs, err := w.MintTokens(mintResponse.Quote) if err != nil { @@ -394,6 +424,7 @@ func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { } } +// Melt will request the mint to pay the given invoice func (w *Wallet) Melt(invoice string, mint string) (*nut05.PostMeltBolt11Response, error) { selectedMint, ok := w.mints[mint] if !ok { @@ -428,6 +459,8 @@ func (w *Wallet) Melt(invoice string, mint string) (*nut05.PostMeltBolt11Respons return meltBolt11Response, nil } +// GetProofsByMint will return an array of proofs that are from +// the passed mint func (w *Wallet) GetProofsByMint(mintURL string) (cashu.Proofs, error) { selectedMint, ok := w.mints[mintURL] if !ok { @@ -448,6 +481,8 @@ func (w *Wallet) GetProofsByMint(mintURL string) (cashu.Proofs, error) { return proofs, nil } +// getProofsForAmount will return proofs from mint that equal to given amount. +// It returns error if wallet does not have enough proofs to fulfill amount func (w *Wallet) getProofsForAmount(amount uint64, mintURL string) (cashu.Proofs, error) { selectedMint, ok := w.mints[mintURL] if !ok { @@ -460,7 +495,6 @@ func (w *Wallet) getProofsForAmount(amount uint64, mintURL string) (cashu.Proofs return nil, errors.New("not enough funds in selected mint") } - // use proofs from inactive keysets first activeKeysetProofs := cashu.Proofs{} inactiveKeysetProofs := cashu.Proofs{} mintProofs, err := w.GetProofsByMint(mintURL) @@ -468,6 +502,7 @@ func (w *Wallet) getProofsForAmount(amount uint64, mintURL string) (cashu.Proofs return nil, err } + // use proofs from inactive keysets first for _, proof := range mintProofs { isInactive := false for _, inactiveKeyset := range selectedMint.inactiveKeysets { @@ -623,6 +658,7 @@ func (w *Wallet) CreateBlindedMessages(amount uint64, keyset crypto.Keyset) (cas return blindedMessages, secrets, rs, nil } +// ConstructProofs unblinds the blindedSignatures and returns the proofs func (w *Wallet) ConstructProofs(blindedSignatures cashu.BlindedSignatures, secrets []string, rs []*secp256k1.PrivateKey, keyset *crypto.Keyset) (cashu.Proofs, error) { @@ -685,6 +721,7 @@ func (w *Wallet) getWalletMints() map[string]walletMint { return walletMints } +// CurrentMint returns the current mint url func (w *Wallet) CurrentMint() string { return w.currentMint.mintURL }