diff --git a/cmd/cashu/feni.go b/cmd/cashu/feni.go index 7ceeaa6..dd4f973 100644 --- a/cmd/cashu/feni.go +++ b/cmd/cashu/feni.go @@ -1,61 +1,40 @@ package main import ( - "fmt" "os" - "regexp" "strings" "github.com/c-bata/go-prompt" "github.com/cashubtc/cashu-feni/cmd/cashu/feni" ) -var sendRegex = regexp.MustCompile("send [0-9]") var advancedPrompt = &feni.CobraPrompt{ RootCmd: feni.RootCmd, PersistFlagValues: true, ShowHelpCommandAndFlags: false, DisableCompletionCommand: true, AddDefaultExitCommand: false, - + DynamicSuggestionsFunc: feni.DynamicSuggestion(feni.RootCmd), GoPromptOptions: []prompt.Option{ prompt.OptionTitle("cashu-feni"), prompt.OptionPrefix(">(cashu-feni)> "), prompt.OptionMaxSuggestion(10), }, - DynamicSuggestionsFunc: func(annotationValue string, document *prompt.Document) []prompt.Suggest { - if document.Text == "-w " || document.Text == "--wallet " { - fmt.Println(document.Text) - if suggestions := feni.GetWalletsDynamic(annotationValue); suggestions != nil { - return suggestions - } - } else if document.Text == "locks " || document.Text == "-l " { - if suggestions := feni.GetLocksDynamic(annotationValue); suggestions != nil { - return suggestions - } - } else if sendRegex.MatchString(document.Text) { - document.Text = fmt.Sprintf("%s %s", document.Text, "-m ") - if suggestions := feni.GetMintsDynamic(annotationValue); suggestions != nil { - return suggestions - } - } - return nil - }, OnErrorFunc: func(err error) { if strings.Contains(err.Error(), "unknown command") { - feni.RootCmd.PrintErrln(err) + feni.RootCmd.Command().PrintErrln(err) return } - feni.RootCmd.PrintErr(err) + feni.RootCmd.Command().PrintErr(err) os.Exit(1) }, } func main() { - feni.StartClientConfiguration() - advancedPrompt.RootCmd.PersistentFlags().StringVarP(&feni.WalletUsed, "wallet", "w", "wallet", "Name of your wallet") - advancedPrompt.RootCmd.PersistentFlags().StringVarP(&feni.Host, "host", "H", "", "Mint host address") + + advancedPrompt.RootCmd.Command().PersistentFlags().StringVarP(&feni.WalletName, "wallet", "w", "wallet", "Name of your wallet") + advancedPrompt.RootCmd.Command().PersistentFlags().StringVarP(&advancedPrompt.RootCmd.Wallet().Config.MintServerHost, "host", "H", "", "Mint host address") advancedPrompt.Run() } diff --git a/cmd/cashu/feni/balance.go b/cmd/cashu/feni/balance.go index 6358f28..05bba04 100644 --- a/cmd/cashu/feni/balance.go +++ b/cmd/cashu/feni/balance.go @@ -2,23 +2,30 @@ package feni import ( "fmt" + "github.com/cashubtc/cashu-feni/wallet" "github.com/spf13/cobra" ) func init() { - RootCmd.AddCommand(balanceCommand) + RootCmd.Command().AddCommand(balanceCommand) } var balanceCommand = &cobra.Command{ - Use: "balance", - Short: "Check your balance", - Long: ``, - PreRun: PreRunFeni, - Run: balance, + Use: "balance", + Short: "Check your balance", + Long: ``, + PreRun: RunCommandWithWallet(RootCmd, func(w *wallet.Wallet, params cobraParameter) { + opts := make([]wallet.Option, 0) + if WalletName != "" { + opts = append(opts, wallet.WithName(WalletName)) + } + RootCmd.wallet = wallet.New(opts...) + }), + Run: RunCommandWithWallet(RootCmd, balance), } -func balance(cmd *cobra.Command, args []string) { - balances, err := Wallet.balancePerKeySet() +func balance(wallet *wallet.Wallet, params cobraParameter) { + balances, err := wallet.Balances() if err != nil { panic(err) } diff --git a/cmd/cashu/feni/burn.go b/cmd/cashu/feni/burn.go index 9032c37..79d0f33 100644 --- a/cmd/cashu/feni/burn.go +++ b/cmd/cashu/feni/burn.go @@ -4,16 +4,16 @@ import ( "encoding/base64" "encoding/json" "github.com/cashubtc/cashu-feni/cashu" + "github.com/cashubtc/cashu-feni/wallet" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var burnCommand = &cobra.Command{ - Use: "burn", - Short: "Burn spent tokens", - Long: ``, - PreRun: PreRunFeni, - Run: burnCmd, + Use: "burn", + Short: "Burn spent tokens", + Long: ``, + Run: RunCommandWithWallet(RootCmd, burnCmd), } var all bool var force bool @@ -21,15 +21,15 @@ var force bool func init() { burnCommand.PersistentFlags().BoolVarP(&all, "all", "a", false, "burn all spent tokens.") burnCommand.PersistentFlags().BoolVarP(&force, "force", "f", false, "force check on all tokens.") - RootCmd.AddCommand(burnCommand) + RootCmd.Command().AddCommand(burnCommand) } -func burnCmd(cmd *cobra.Command, args []string) { +func burnCmd(wallet *wallet.Wallet, params cobraParameter) { var token string - if len(args) == 1 { - token = args[0] + if len(params.args) == 1 { + token = params.args[0] } if !(all || force || token != "") || (token != "" && all) { - cmd.Println("Error: enter a token or use --all to burn all pending tokens or --force to check all tokens.") + params.cmd.Println("Error: enter a token or use --all to burn all pending tokens or --force to check all tokens.") return } proofs := make([]cashu.Proof, 0) @@ -40,7 +40,7 @@ func burnCmd(cmd *cobra.Command, args []string) { log.Fatal(err) } } else if force { - proofs = Wallet.proofs + // invalidate all wallet proofs } else { p, err := base64.URLEncoding.DecodeString(token) if err != nil { @@ -56,7 +56,7 @@ func burnCmd(cmd *cobra.Command, args []string) { return } - err = invalidate(proofs) + err = wallet.Invalidate(proofs) if err != nil { log.Fatal(err) } diff --git a/cmd/cashu/feni/cmd.go b/cmd/cashu/feni/cmd.go new file mode 100644 index 0000000..91e2f99 --- /dev/null +++ b/cmd/cashu/feni/cmd.go @@ -0,0 +1,32 @@ +package feni + +import ( + "github.com/cashubtc/cashu-feni/wallet" + "github.com/spf13/cobra" +) + +type command func(wallet *wallet.Wallet, params cobraParameter) +type cobraParameter struct { + cmd *cobra.Command + args []string +} +type RootCommand struct { + cmd *cobra.Command + wallet *wallet.Wallet +} + +func (r *RootCommand) SetCommand(cmd *cobra.Command) { + r.cmd = cmd +} +func (r *RootCommand) Command() *cobra.Command { + return r.cmd +} +func (r *RootCommand) Wallet() *wallet.Wallet { + return r.wallet +} +func RunCommandWithWallet(root *RootCommand, command command) func(cmd *cobra.Command, args []string) { + return func(cmd *cobra.Command, args []string) { + cmd.ParseFlags(args) + command(root.wallet, cobraParameter{cmd: cmd, args: args}) + } +} diff --git a/cmd/cashu/feni/config.go b/cmd/cashu/feni/config.go index da95628..3cef51c 100644 --- a/cmd/cashu/feni/config.go +++ b/cmd/cashu/feni/config.go @@ -1,188 +1,7 @@ package feni import ( - "fmt" - "github.com/caarlos0/env/v6" - "github.com/cashubtc/cashu-feni/cashu" - "github.com/cashubtc/cashu-feni/crypto" - "github.com/cashubtc/cashu-feni/db" - "github.com/cashubtc/cashu-feni/lightning" - "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/joho/godotenv" - "github.com/samber/lo" - log "github.com/sirupsen/logrus" - "math/rand" - "os" - "path" - "time" + "github.com/cashubtc/cashu-feni/wallet" ) -var Config WalletConfig - -type WalletConfig struct { - Debug bool `env:"DEBUG"` - Lightning bool `env:"LIGHTNING"` - MintServerHost string `env:"MINT_HOST"` - MintServerPort string `env:"MINT_PORT"` - Wallet string `env:"WALLET"` -} - -func defaultConfig() { - log.Infof("Loading default configuration") - Config = WalletConfig{ - Debug: true, - Lightning: true, - MintServerHost: "https://8333.space", - MintServerPort: "3339", - Wallet: "wallet", - } - -} -func StartClientConfiguration() { - loaded := false - dirname, err := os.UserHomeDir() - if err != nil { - log.Fatal(err) - } - p := path.Join(dirname, ".cashu", ".env") - err = godotenv.Load(p) - if err != nil { - defaultConfig() - loaded = true - } - if !loaded { - err = env.Parse(&Config) - if err != nil { - defaultConfig() - } - } - - // initialize the default wallet (no other option selected using -w) - lightning.Config.Lightning.Enabled = Config.Lightning - InitializeDatabase(Config.Wallet) - - rand.Seed(time.Now().UnixNano()) - - Wallet = MintWallet{ - proofs: make([]cashu.Proof, 0), - Client: &Client{Url: fmt.Sprintf("%s:%s", Config.MintServerHost, Config.MintServerPort)}, - } - - Wallet.loadDefaultMint() - -} -func (w *MintWallet) loadMint(keySetId string) { - /*keySet, err := storage.GetKeySet(db.KeySetWithId(keySetId)) - if err != nil { - panic(err) - } - */ - for _, set := range w.keySets { - if set.Id == keySetId { - w.currentKeySet = &set - } - } - w.Client.Url = w.currentKeySet.MintUrl - w.loadDefaultMint() -} -func (w *MintWallet) setCurrentKeySet(keySet crypto.KeySet) { - for _, set := range w.keySets { - if set.Id == keySet.Id { - w.currentKeySet = &keySet - } - } -} -func (w *MintWallet) loadPersistedKeySets() { - persistedKeySets, err := storage.GetKeySet() - if err != nil { - panic(err) - } - w.keySets = persistedKeySets -} -func (w *MintWallet) loadDefaultMint() { - keySet, _ := w.persistCurrentKeysSet() - w.loadPersistedKeySets() - w.setCurrentKeySet(keySet) - k, err := w.Client.KeySets() - if err != nil { - panic(err) - } - for _, set := range k.KeySets { - if _, found := lo.Find[crypto.KeySet](w.keySets, func(k crypto.KeySet) bool { - return set == k.Id - }); !found { - err = w.checkAndPersistKeySet(set) - if err != nil { - panic(err) - } - } - } - -} -func (w *MintWallet) persistCurrentKeysSet() (crypto.KeySet, error) { - activeKeys, err := w.Client.Keys() - if err != nil { - panic(err) - } - return w.persistKeysSet(activeKeys) -} -func (w *MintWallet) persistKeysSet(keys map[uint64]*secp256k1.PublicKey) (crypto.KeySet, error) { - keySet := crypto.KeySet{MintUrl: w.Client.Url, FirstSeen: time.Now(), PublicKeys: crypto.PublicKeyList{}} - keySet.SetPublicKeyList(keys) - keySet.DeriveKeySetId() - err := storage.StoreKeySet(keySet) - if err != nil { - return keySet, err - } - return keySet, nil -} -func (w *MintWallet) checkAndPersistKeySet(id string) error { - var ks []crypto.KeySet - var err error - if ks, err = storage.GetKeySet(db.KeySetWithId(id)); err != nil || len(ks) == 0 { - keys, err := w.Client.KeysForKeySet(id) - if err != nil { - return err - } - k, err := w.persistKeysSet(keys) - ks = append(ks, k) - if err != nil { - return err - } - } - Wallet.keySets = append(Wallet.keySets, ks...) - return nil -} -func InitializeDatabase(wallet string) { - dirname, err := os.UserHomeDir() - if err != nil { - log.Fatal(err) - } - walletPath := path.Join(dirname, ".cashu", wallet) - db.Config.Database.Sqlite = &db.SqliteConfig{Path: walletPath, FileName: "wallet.sqlite3"} - err = env.Parse(&Config) - if err != nil { - panic(err) - } - storage = db.NewSqlDatabase() - err = storage.Migrate(cashu.Proof{}) - if err != nil { - panic(err) - } - err = storage.Migrate(cashu.ProofsUsed{}) - if err != nil { - panic(err) - } - err = storage.Migrate(crypto.KeySet{}) - if err != nil { - panic(err) - } - err = storage.Migrate(cashu.P2SHScript{}) - if err != nil { - panic(err) - } - err = storage.Migrate(cashu.CreateInvoice()) - if err != nil { - panic(err) - } -} +var Config wallet.Config diff --git a/cmd/cashu/feni/feni.go b/cmd/cashu/feni/feni.go index ea19f3a..21ac719 100644 --- a/cmd/cashu/feni/feni.go +++ b/cmd/cashu/feni/feni.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/c-bata/go-prompt" "github.com/cashubtc/cashu-feni/db" + "github.com/cashubtc/cashu-feni/wallet" "github.com/joho/godotenv" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -11,9 +12,6 @@ import ( "path" ) -var WalletUsed string -var Host string - var storage db.MintStorage const getWalletsAnnotationValue = "GetWallets" @@ -38,25 +36,16 @@ var GetWalletsDynamic = func(annotationValue string) []prompt.Suggest { return suggestions } -func PreRunFeni(cmd *cobra.Command, args []string) { - // Do not initialize default wallet again - InitializeDatabase(WalletUsed) - if storage != nil { - var err error - Wallet.proofs, err = storage.GetUsedProofs() - if err != nil { - panic(err) - } - } -} - -var RootCmd = &cobra.Command{ - Use: "feni", - Short: "Cashu Feni is a cashu wallet application", - Long: ``, - Annotations: map[string]string{ - DynamicSuggestionsAnnotation: getWalletsAnnotationValue, - }, - Run: func(cmd *cobra.Command, args []string) { +var RootCmd = &RootCommand{ + wallet: &wallet.Wallet{}, + cmd: &cobra.Command{ + Use: "feni", + Short: "Cashu Feni is a cashu wallet application", + Long: ``, + Annotations: map[string]string{ + DynamicSuggestionsAnnotation: getWalletsAnnotationValue, + }, + Run: func(cmd *cobra.Command, args []string) { + }, }, } diff --git a/cmd/cashu/feni/invoice.go b/cmd/cashu/feni/invoice.go index 511fa89..cfd7540 100644 --- a/cmd/cashu/feni/invoice.go +++ b/cmd/cashu/feni/invoice.go @@ -2,11 +2,9 @@ package feni import ( "fmt" - "github.com/cashubtc/cashu-feni/cashu" "github.com/cashubtc/cashu-feni/db" "github.com/cashubtc/cashu-feni/lightning" - "github.com/cashubtc/cashu-feni/mint" - "github.com/samber/lo" + "github.com/cashubtc/cashu-feni/wallet" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "strconv" @@ -14,34 +12,32 @@ import ( ) var invoiceCommand = &cobra.Command{ - Use: "invoice", - Short: "Creates a new invoice, if lightning is enabled", - Long: ``, - PreRun: PreRunFeni, - Run: mintCmd, + Use: "invoice", + Short: "Creates a new invoice, if lightning is enabled", + Long: ``, + Run: RunCommandWithWallet(RootCmd, mintCmd), } var hash string func init() { invoiceCommand.PersistentFlags().StringVarP(&hash, "hash", "", "", "the hash of the mint you want to claim") - RootCmd.AddCommand(invoiceCommand) + RootCmd.Command().AddCommand(invoiceCommand) } -func mintCmd(cmd *cobra.Command, args []string) { - amount, err := strconv.Atoi(args[0]) +func mintCmd(wallet *wallet.Wallet, params cobraParameter) { + amount, err := strconv.Atoi(params.args[0]) if err != nil { panic(err) } - splitAmount := mint.AmountSplit(uint64(amount)) if amount > 0 { if !Config.Lightning { - if err := storeProofs(Wallet.mint(splitAmount, hash)); err != nil { + if _, err := wallet.Mint(uint64(amount), hash); err != nil { log.Error(err) } return } if hash == "" { var invoice lightning.Invoicer - invoice, err = Wallet.Client.GetMint(int64(amount)) + invoice, err = wallet.Client.GetMint(int64(amount)) if err != nil { panic(err) } @@ -56,77 +52,24 @@ func mintCmd(cmd *cobra.Command, args []string) { fmt.Printf("Checking invoice ...") for { time.Sleep(time.Second * 3) - proofs := Wallet.mint(splitAmount, invoice.GetHash()) - if len(proofs) == 0 { + proofs, err := wallet.Mint(uint64(amount), invoice.GetHash()) + if err != nil { fmt.Print(".") continue } - // storeProofs - err = storeProofs(proofs) - if err != nil { - log.Error(err.Error()) + if len(proofs) == 0 { + fmt.Print(".") + continue } fmt.Println("Invoice paid.") - err = storage.UpdateLightningInvoice(invoice.GetHash(), db.UpdateInvoicePaid(true)) + err = wallet.Storage.UpdateLightningInvoice(invoice.GetHash(), db.UpdateInvoicePaid(true)) if err != nil { log.Fatal(err) } return } } else { - Wallet.Mint(uint64(amount), hash) - } - } -} - -func invalidate(proofs []cashu.Proof) error { - resp, err := Wallet.Client.Check(cashu.CheckSpendableRequest{Proofs: proofs}) - if err != nil { - return err - } - invalidatedProofs := make([]cashu.Proof, 0) - for i, spendable := range resp.Spendable { - if !spendable { - invalidatedProofs = append(invalidatedProofs, proofs[i]) - err = invalidateProof(proofs[i]) - if err != nil { - return err - } - } - } - invalidatedSecrets := make([]string, 0) - for _, proof := range invalidatedProofs { - invalidatedSecrets = append(invalidatedSecrets, proof.Secret) - } - Wallet.proofs = lo.Filter[cashu.Proof](Wallet.proofs, func(p cashu.Proof, i int) bool { - _, found := lo.Find[string](invalidatedSecrets, func(secret string) bool { - return secret == p.Secret - }) - return !found - }) - return nil -} -func invalidateProof(proof cashu.Proof) error { - err := storage.DeleteProof(proof) - if err != nil { - return err - } - return storage.StoreUsedProofs( - cashu.ProofsUsed{ - Secret: proof.Secret, - Amount: proof.Amount, - C: proof.C, - TimeUsed: time.Now(), - }, - ) -} -func storeProofs(proofs []cashu.Proof) error { - for _, proof := range proofs { - Wallet.proofs = append(Wallet.proofs, proof) - err := storage.StoreProof(proof) - if err != nil { - return err + wallet.Mint(uint64(amount), hash) } } - return nil } diff --git a/cmd/cashu/feni/invoices.go b/cmd/cashu/feni/invoices.go index 8a79860..d7879d8 100644 --- a/cmd/cashu/feni/invoices.go +++ b/cmd/cashu/feni/invoices.go @@ -3,25 +3,25 @@ package feni import ( "fmt" "github.com/cashubtc/cashu-feni/lightning/invoice" + "github.com/cashubtc/cashu-feni/wallet" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var invoicesCommand = &cobra.Command{ - Use: "invoices", - Short: "List all pending invoices", - Long: ``, - PreRun: PreRunFeni, - Run: invoicesCmd, + Use: "invoices", + Short: "List all pending invoices", + Long: ``, + Run: RunCommandWithWallet(RootCmd, invoicesCmd), } func init() { - RootCmd.AddCommand(invoicesCommand) + RootCmd.Command().AddCommand(invoicesCommand) } -func invoicesCmd(cmd *cobra.Command, args []string) { +func invoicesCmd(wallet *wallet.Wallet, params cobraParameter) { invoices := make([]invoice.Invoice, 0) - invoices, err := storage.GetLightningInvoices(false) + invoices, err := wallet.Storage.GetLightningInvoices(false) if err != nil { log.Fatal(err) } diff --git a/cmd/cashu/feni/lock.go b/cmd/cashu/feni/lock.go index 55ae58a..6677fa6 100644 --- a/cmd/cashu/feni/lock.go +++ b/cmd/cashu/feni/lock.go @@ -5,27 +5,27 @@ import ( "fmt" "github.com/cashubtc/cashu-feni/bitcoin" "github.com/cashubtc/cashu-feni/cashu" + "github.com/cashubtc/cashu-feni/wallet" "github.com/spf13/cobra" ) func init() { - RootCmd.AddCommand(lockCommand) + RootCmd.Command().AddCommand(lockCommand) } var lockCommand = &cobra.Command{ - Use: "lock", - Short: "Generate receiving lock", - Long: `Generates a receiving lock for cashu tokens.`, - PreRun: PreRunFeni, - Run: lock, + Use: "lock", + Short: "Generate receiving lock", + Long: `Generates a receiving lock for cashu tokens.`, + Run: RunCommandWithWallet(RootCmd, lock), } func flagIsPay2ScriptHash() bool { return cashu.IsPay2ScriptHash(lockFlag) } -func lock(cmd *cobra.Command, args []string) { +func lock(wallet *wallet.Wallet, params cobraParameter) { fmt.Println(createP2SHLock()) } diff --git a/cmd/cashu/feni/locks.go b/cmd/cashu/feni/locks.go index 8c58e1d..a592521 100644 --- a/cmd/cashu/feni/locks.go +++ b/cmd/cashu/feni/locks.go @@ -4,11 +4,12 @@ import ( "fmt" "github.com/c-bata/go-prompt" "github.com/cashubtc/cashu-feni/cashu" + "github.com/cashubtc/cashu-feni/wallet" "github.com/spf13/cobra" ) func init() { - RootCmd.AddCommand(locksCommand) + RootCmd.Command().AddCommand(locksCommand) } @@ -27,17 +28,16 @@ var GetLocksDynamic = func(annotationValue string) []prompt.Suggest { return suggestions } var locksCommand = &cobra.Command{ - Use: "locks", - Short: "Show unused receiving locks", - Long: `Generates a receiving lock for cashu tokens.`, - PreRun: PreRunFeni, + Use: "locks", + Short: "Show unused receiving locks", + Long: `Generates a receiving lock for cashu tokens.`, Annotations: map[string]string{ DynamicSuggestionsAnnotation: getLocksAnnotationValue, }, - Run: locks, + Run: RunCommandWithWallet(RootCmd, locks), } -func locks(cmd *cobra.Command, args []string) { +func locks(wallet *wallet.Wallet, params cobraParameter) { scriptLocks := getP2SHLocks() for _, l := range scriptLocks { fmt.Printf("P2SH:%s\n", l.Address) diff --git a/cmd/cashu/feni/pay.go b/cmd/cashu/feni/pay.go index 24cc43a..5362334 100644 --- a/cmd/cashu/feni/pay.go +++ b/cmd/cashu/feni/pay.go @@ -3,6 +3,7 @@ package feni import ( "fmt" "github.com/cashubtc/cashu-feni/cashu" + "github.com/cashubtc/cashu-feni/wallet" decodepay "github.com/nbd-wtf/ln-decodepay" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -11,16 +12,15 @@ import ( ) func init() { - RootCmd.AddCommand(payCommand) + RootCmd.Command().AddCommand(payCommand) } var payCommand = &cobra.Command{ - Use: "pay ", - Short: "Pay lightning invoice", - Long: `Pay a lightning invoice using cashu tokens.`, - PreRun: PreRunFeni, - Run: pay, + Use: "pay ", + Short: "Pay lightning invoice", + Long: `Pay a lightning invoice using cashu tokens.`, + Run: RunCommandWithWallet(RootCmd, pay), } func ask(cmd *cobra.Command) bool { @@ -50,42 +50,42 @@ func ask(cmd *cobra.Command) bool { } return false } -func pay(cmd *cobra.Command, args []string) { - if len(args) != 1 { - cmd.Help() +func pay(wallet *wallet.Wallet, params cobraParameter) { + if len(params.args) != 1 { + params.cmd.Help() return } - invoice := args[0] - fee, err := Wallet.Client.CheckFee(cashu.CheckFeesRequest{Pr: invoice}) + invoice := params.args[0] + fee, err := wallet.Client.CheckFee(cashu.CheckFeesRequest{Pr: invoice}) if err != nil { log.Fatal(err) } bold, err := decodepay.Decodepay(invoice) if err != nil { - cmd.Println("invalid invoice") + params.cmd.Println("invalid invoice") return } amount := math.Ceil(float64((uint64(bold.MSatoshi) + fee.Fee*1000) / 1000)) if amount < 0 { log.Fatal("amount is not positive") } - if Wallet.availableBalance() < uint64(amount) { + if wallet.AvailableBalance() < uint64(amount) { log.Fatal("Error: Balance to low.") } - cmd.Printf("Pay %d sat (%f sat incl. fees)?\n", uint64(amount)-fee.Fee, amount) - cmd.Println("continue? [Y/n]") - if !ask(cmd) { - cmd.Println("canceled...") + params.cmd.Printf("Pay %d sat (%f sat incl. fees)?\n", uint64(amount)-fee.Fee, amount) + params.cmd.Println("continue? [Y/n]") + if !ask(params.cmd) { + params.cmd.Println("canceled...") return } - _, sendProofs, err := Wallet.SplitToSend(uint64(amount), "", false) + _, sendProofs, err := wallet.SplitToSend(uint64(amount), "", false) if err != nil { log.Fatal(err) } log.Infof("Paying Lightning invoice ...") - changeProofs, err := Wallet.PayLightning(sendProofs, invoice) + changeProofs, err := wallet.PayLightning(sendProofs, invoice) if changeProofs != nil { - err = storeProofs(changeProofs) + err = wallet.StoreProofs(changeProofs) if err != nil { log.Fatal(err) } diff --git a/cmd/cashu/feni/pending.go b/cmd/cashu/feni/pending.go index edf78ab..c2ae50b 100644 --- a/cmd/cashu/feni/pending.go +++ b/cmd/cashu/feni/pending.go @@ -2,22 +2,22 @@ package feni import ( "fmt" + "github.com/cashubtc/cashu-feni/wallet" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var pendingCommand = &cobra.Command{ - Use: "pending", - Short: "Show pending tokens", - Long: ``, - PreRun: PreRunFeni, - Run: pendingCmd, + Use: "pending", + Short: "Show pending tokens", + Long: ``, + Run: RunCommandWithWallet(RootCmd, pendingCmd), } func init() { - RootCmd.AddCommand(pendingCommand) + RootCmd.Command().AddCommand(pendingCommand) } -func pendingCmd(cmd *cobra.Command, args []string) { +func pendingCmd(wallet *wallet.Wallet, params cobraParameter) { reserved, err := storage.GetReservedProofs() if err != nil { log.Fatal(err) diff --git a/cmd/cashu/feni/prompt.go b/cmd/cashu/feni/prompt.go index 3489920..85f4d00 100644 --- a/cmd/cashu/feni/prompt.go +++ b/cmd/cashu/feni/prompt.go @@ -1,10 +1,13 @@ package feni import ( + "fmt" "github.com/c-bata/go-prompt" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/spf13/pflag" "os" + "regexp" "strings" ) @@ -14,11 +17,14 @@ const DynamicSuggestionsAnnotation = "cobra-prompt-dynamic-suggestions" // PersistFlagValuesFlag the flag that will be avaiailable when PersistFlagValues is true const PersistFlagValuesFlag = "persist-flag-values" +var sendRegex = regexp.MustCompile("send [0-9]") +var WalletName string + // CobraPrompt given a Cobra command it will make every flag and sub commands available as suggestions. // Command.Short will be used as description for the suggestion. type CobraPrompt struct { // RootCmd is the start point, all its sub commands and flags will be available as suggestions - RootCmd *cobra.Command + RootCmd *RootCommand // GoPromptOptions is for customize go-prompt // see https://github.com/c-bata/go-prompt/blob/master/option.go @@ -56,6 +62,28 @@ type CobraPrompt struct { SuggestionFilter func(suggestions []prompt.Suggest, document *prompt.Document) []prompt.Suggest } +func DynamicSuggestion(cmd *RootCommand) func(annotationValue string, document *prompt.Document) []prompt.Suggest { + return func(annotationValue string, document *prompt.Document) []prompt.Suggest { + if document.Text == "-w " || document.Text == "--wallet " { + log.Println(document.Text) + if suggestions := GetWalletsDynamic(annotationValue); suggestions != nil { + return suggestions + } + } else if document.Text == "locks " || document.Text == "-l " { + if suggestions := GetLocksDynamic(annotationValue); suggestions != nil { + return suggestions + } + } else if sendRegex.MatchString(document.Text) { + document.Text = fmt.Sprintf("%s %s", document.Text, "-m ") + if suggestions := GetMintsDynamic(cmd.wallet, annotationValue); suggestions != nil { + return suggestions + } + } + return nil + } + +} + // Run will automatically generate suggestions for all cobra commands and flags defined by RootCmd // and execute the selected commands. Run will also reset all given flags by default, see PersistFlagValues func (co CobraPrompt) Run() { @@ -64,19 +92,20 @@ func (co CobraPrompt) Run() { } co.prepare() - co.RootCmd.SetIn(os.Stdin) + co.RootCmd.Command().SetIn(os.Stdin) p := prompt.New( func(in string) { go func() { }() promptArgs := co.parseArgs(in) + os.Args = append([]string{os.Args[0]}, promptArgs...) - if err := co.RootCmd.Execute(); err != nil { + if err := co.RootCmd.Command().Execute(); err != nil { if co.OnErrorFunc != nil { co.OnErrorFunc(err) } else { - co.RootCmd.PrintErrln(err) + co.RootCmd.Command().PrintErrln(err) os.Exit(1) } } @@ -101,15 +130,15 @@ func (co CobraPrompt) parseArgs(in string) []string { func (co CobraPrompt) prepare() { if co.ShowHelpCommandAndFlags { // TODO: Add suggestions for help command - co.RootCmd.InitDefaultHelpCmd() + co.RootCmd.Command().InitDefaultHelpCmd() } if co.DisableCompletionCommand { - co.RootCmd.CompletionOptions.DisableDefaultCmd = true + co.RootCmd.Command().CompletionOptions.DisableDefaultCmd = true } if co.AddDefaultExitCommand { - co.RootCmd.AddCommand(&cobra.Command{ + co.RootCmd.Command().AddCommand(&cobra.Command{ Use: "exit", Short: "Exit prompt", Run: func(cmd *cobra.Command, args []string) { @@ -119,7 +148,7 @@ func (co CobraPrompt) prepare() { } if co.PersistFlagValues { - co.RootCmd.PersistentFlags().BoolP(PersistFlagValuesFlag, "", + co.RootCmd.Command().PersistentFlags().BoolP(PersistFlagValuesFlag, "", false, "Persist last given value for flags") } } @@ -128,12 +157,12 @@ func findSuggestions(co *CobraPrompt, d *prompt.Document) []prompt.Suggest { command := co.RootCmd args := strings.Fields(d.CurrentLine()) - if found, _, err := command.Find(args); err == nil { - command = found + if found, _, err := command.Command().Find(args); err == nil { + command.SetCommand(found) } var suggestions []prompt.Suggest - persistFlagValues, _ := command.Flags().GetBool(PersistFlagValuesFlag) + persistFlagValues, _ := command.Command().Flags().GetBool(PersistFlagValuesFlag) addFlags := func(flag *pflag.Flag) { if flag.Changed && !persistFlagValues { flag.Value.Set(flag.DefValue) @@ -148,11 +177,11 @@ func findSuggestions(co *CobraPrompt, d *prompt.Document) []prompt.Suggest { } } - command.LocalFlags().VisitAll(addFlags) - command.InheritedFlags().VisitAll(addFlags) + command.Command().LocalFlags().VisitAll(addFlags) + command.Command().InheritedFlags().VisitAll(addFlags) - if command.HasAvailableSubCommands() { - for _, c := range command.Commands() { + if command.Command().HasAvailableSubCommands() { + for _, c := range command.Command().Commands() { if !c.Hidden && !co.ShowHiddenCommands { suggestions = append(suggestions, prompt.Suggest{Text: c.Name(), Description: c.Short}) } @@ -162,7 +191,7 @@ func findSuggestions(co *CobraPrompt, d *prompt.Document) []prompt.Suggest { } } - annotation := command.Annotations[DynamicSuggestionsAnnotation] + annotation := command.Command().Annotations[DynamicSuggestionsAnnotation] if co.DynamicSuggestionsFunc != nil && annotation != "" { sugs := co.DynamicSuggestionsFunc(annotation, d) if sugs != nil { diff --git a/cmd/cashu/feni/receive.go b/cmd/cashu/feni/receive.go index e2a2939..8c69a65 100644 --- a/cmd/cashu/feni/receive.go +++ b/cmd/cashu/feni/receive.go @@ -5,23 +5,23 @@ import ( "encoding/json" "fmt" "github.com/cashubtc/cashu-feni/cashu" + "github.com/cashubtc/cashu-feni/wallet" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "strings" ) func init() { - RootCmd.AddCommand(receiveCommand) + RootCmd.Command().AddCommand(receiveCommand) receiveCommand.PersistentFlags().StringVarP(&lockFlag, "lock", "l", "", "Lock tokens (P2SH)") } var receiveCommand = &cobra.Command{ - Use: "receive", - Short: "Receive tokens", - Long: `Receive cashu tokens from another user`, - PreRun: PreRunFeni, - Run: receive, + Use: "receive", + Short: "Receive tokens", + Long: `Receive cashu tokens from another user`, + Run: RunCommandWithWallet(RootCmd, receive), } type Tokens struct { @@ -65,21 +65,15 @@ func NewTokens(t string) *Tokens { return token } -type Mint struct { - URL string `json:"url"` - Ks []string `json:"ks"` -} -type Mints map[string]Mint - -func receive(cmd *cobra.Command, args []string) { +func receive(wallet *wallet.Wallet, params cobraParameter) { var script, signature string - coin := args[0] + coin := params.args[0] if lockFlag != "" { if !flagIsPay2ScriptHash() { log.Fatal("lock has wrong format. Expected P2SH:
") } addressSplit := strings.Split(lockFlag, "P2SH:")[1] - p2shScripts, err := getUnusedLocks(addressSplit) + p2shScripts, err := getUnusedLocks(wallet, addressSplit) if err != nil { log.Fatal(err) } @@ -96,17 +90,31 @@ func receive(cmd *cobra.Command, args []string) { log.Fatal("Aborted!") }*/ for _, token := range tokens.Token { - defaultUrl := Wallet.Client.Url + defaultUrl := wallet.Client.Url defer func() { - Wallet.Client.Url = defaultUrl + wallet.Client.Url = defaultUrl }() - Wallet.Client.Url = token.Mint - _, _, err := Wallet.redeem(token.Proofs, script, signature) + wallet.Client.Url = token.Mint + _, _, err := redeem(wallet, token.Proofs, script, signature) if err != nil { log.Fatal(err) } } } +func redeem(w *wallet.Wallet, proofs []cashu.Proof, scndScript, scndSignature string) (keep []cashu.Proof, send []cashu.Proof, err error) { + if scndScript != "" && scndSignature != "" { + log.Infof("Unlock script: %s", scndScript) + for i := range proofs { + proofs[i].Script = &cashu.P2SHScript{ + Script: scndScript, + Signature: scndSignature} + } + } + return w.Split(proofs, wallet.SumProofs(proofs), "") +} +func getUnusedLocks(wallet *wallet.Wallet, addressSplit string) ([]cashu.P2SHScript, error) { + return wallet.Storage.GetScripts(addressSplit) +} /* func verifyMints(cmd cobra.Command, token Tokens) (trust bool) { diff --git a/cmd/cashu/feni/send.go b/cmd/cashu/feni/send.go index b3d237c..67b3e7d 100644 --- a/cmd/cashu/feni/send.go +++ b/cmd/cashu/feni/send.go @@ -3,6 +3,7 @@ package feni import ( "bytes" "fmt" + "github.com/cashubtc/cashu-feni/wallet" "strconv" "github.com/c-bata/go-prompt" @@ -13,31 +14,30 @@ import ( ) func init() { - RootCmd.AddCommand(sendCommand) + RootCmd.Command().AddCommand(sendCommand) sendCommand.PersistentFlags().StringVarP(&lockFlag, "lock", "l", "", "Lock tokens (P2SH)") } var lockFlag string var sendCommand = &cobra.Command{ - Use: "send ", - Short: "Send tokens", - Long: `Send cashu tokens to another user`, - PreRun: PreRunFeni, + Use: "send ", + Short: "Send tokens", + Long: `Send cashu tokens to another user`, Annotations: map[string]string{ DynamicSuggestionsAnnotation: getLocksAnnotationValue, // get suggestion for p2sh }, - Run: send, + Run: RunCommandWithWallet(RootCmd, send), } var filteredKeySets []crypto.KeySet -var GetMintsDynamic = func(annotationValue string) []prompt.Suggest { +var GetMintsDynamic = func(wallet *wallet.Wallet, annotationValue string) []prompt.Suggest { keysets, err := storage.GetKeySet() if err != nil { return nil } suggestions := make([]prompt.Suggest, 0) setBalanceAvailable := make(map[string]uint64) - balances, err := Wallet.balancePerKeySet() + balances, err := wallet.Balances() if err != nil { panic(err) } @@ -57,14 +57,14 @@ var GetMintsDynamic = func(annotationValue string) []prompt.Suggest { return suggestions } -func askMintSelection(cmd *cobra.Command) error { +func askMintSelection(wallet *wallet.Wallet, cmd *cobra.Command) error { keysets, err := storage.GetKeySet() if err != nil { return nil } setBalance := make(map[string]uint64) setBalanceAvailable := make(map[string]uint64) - balances, err := Wallet.balancePerKeySet() + balances, err := wallet.Balances() if err != nil { panic(err) } @@ -78,8 +78,8 @@ func askMintSelection(cmd *cobra.Command) error { cmd.Printf("Mint: %d Balance: %d sat (available: %d) URL: %s\n", i+1, setBalance[set.MintUrl], setBalanceAvailable[set.MintUrl], set.MintUrl) } cmd.Printf("Select mint [1-%d, press enter default 1]\n\n", len(filteredKeySets)) - Wallet.Client.Url = filteredKeySets[askInt(cmd)-1].MintUrl - Wallet.loadDefaultMint() + wallet.Client.Url = filteredKeySets[askInt(cmd)-1].MintUrl + wallet.LoadDefaultMint() return nil } @@ -105,9 +105,9 @@ func askInt(cmd *cobra.Command) int { return s } -func send(cmd *cobra.Command, args []string) { - if len(args) < 2 { - cmd.Help() +func send(wallet *wallet.Wallet, params cobraParameter) { + if len(params.args) < 2 { + params.cmd.Help() return } if lockFlag != "" && len(lockFlag) < 22 { @@ -119,16 +119,16 @@ func send(cmd *cobra.Command, args []string) { p2sh = true } - mint, _ := strconv.Atoi(args[1]) - Wallet.Client.Url = filteredKeySets[mint].MintUrl - Wallet.loadDefaultMint() + mint, _ := strconv.Atoi(params.args[1]) + wallet.Client.Url = filteredKeySets[mint].MintUrl + wallet.LoadDefaultMint() - amount, err := strconv.ParseUint(args[0], 10, 64) + amount, err := strconv.ParseUint(params.args[0], 10, 64) if err != nil { fmt.Println("invalid amount") return } - _, sendProofs, err := Wallet.SplitToSend(amount, lockFlag, true) + _, sendProofs, err := wallet.SplitToSend(amount, lockFlag, true) if err != nil { fmt.Println(err) return @@ -137,7 +137,7 @@ func send(cmd *cobra.Command, args []string) { if lockFlag != "" && !p2sh { hide = true } - token, err := Wallet.serializeToken(sendProofs, hide) + token, err := serializeToken(wallet.Client, sendProofs, hide) if err != nil { fmt.Println(err) return @@ -149,10 +149,10 @@ func send(cmd *cobra.Command, args []string) { // If the hideSecrets flag is set to true, the Secret field of each proof will be set to an empty string before serialization. // The serialized data is returned as a base64-encoded string. // If an error occurs, the empty string is returned as the result and an error is returned as the second return value. -func (w MintWallet) serializeToken(proofs []cashu.Proof, hideSecrets bool) (string, error) { +func serializeToken(client *wallet.Client, proofs []cashu.Proof, hideSecrets bool) (string, error) { // Create a new Token structure with the given proofs and an empty Mints map. token := Tokens{Token: make([]Token, 0)} - token.Token = append(token.Token, Token{Proofs: proofs, Mint: w.Client.Url}) + token.Token = append(token.Token, Token{Proofs: proofs, Mint: client.Url}) // Iterate over each proof in the `proofs` slice. for i := range proofs { // If `hideSecrets` is true, set the `Secret` field of the current proof to an empty string. diff --git a/mint/mint.go b/mint/mint.go index 1faa76a..b63a8e2 100644 --- a/mint/mint.go +++ b/mint/mint.go @@ -64,7 +64,7 @@ func New(masterKey string, opt ...Options) *Mint { return l } -func (m Mint) setProofsPending(proofs []cashu.Proof) error { +func (m *Mint) setProofsPending(proofs []cashu.Proof) error { for _, proof := range proofs { p, err := m.database.GetUsedProofs(proof.Secret) if err != nil { @@ -83,14 +83,8 @@ func (m Mint) setProofsPending(proofs []cashu.Proof) error { } return nil } -func (m Mint) unsetProofsPending(proofs []cashu.Proof, transactionError *error) error { +func (m *Mint) unsetProofsPending(proofs []cashu.Proof) error { for _, proof := range proofs { - if transactionError != nil { - err := m.database.DeleteProof(proof) - if err != nil { - return err - } - } proof.Status = cashu.ProofStatusSpent err := m.database.StoreProof(proof) if err != nil { @@ -99,7 +93,7 @@ func (m Mint) unsetProofsPending(proofs []cashu.Proof, transactionError *error) } return nil } -func (m Mint) LoadKeySet(id string) (*crypto.KeySet, error) { +func (m *Mint) LoadKeySet(id string) (*crypto.KeySet, error) { if m.keySets[id] == nil { return nil, fmt.Errorf("keyset does not exist") } @@ -141,10 +135,10 @@ func WithStorage(database db.MintStorage) Options { l.database = database } } -func (m Mint) GetKeySetIds() []string { +func (m *Mint) GetKeySetIds() []string { return lo.Keys(m.keySets) } -func (m Mint) GetKeySet() []string { +func (m *Mint) GetKeySet() []string { return lo.Keys(m.keySets) } @@ -228,7 +222,7 @@ func (m *Mint) payLightningInvoice(pr string, feeLimitMSat uint64) (lightning.Pa return m.client.InvoiceStatus(invoice.GetHash()) } -func (m Mint) mint(messages cashu.BlindedMessages, pr string, keySet *crypto.KeySet) ([]cashu.BlindedSignature, error) { +func (m *Mint) mint(messages cashu.BlindedMessages, pr string, keySet *crypto.KeySet) ([]cashu.BlindedSignature, error) { publicKeys := make([]*secp256k1.PublicKey, 0) var amounts []uint64 for _, msg := range messages { @@ -262,11 +256,11 @@ func (m Mint) mint(messages cashu.BlindedMessages, pr string, keySet *crypto.Key return promises, nil } -func (m Mint) Mint(messages cashu.BlindedMessages, pr string, keySet *crypto.KeySet) ([]cashu.BlindedSignature, error) { +func (m *Mint) Mint(messages cashu.BlindedMessages, pr string, keySet *crypto.KeySet) ([]cashu.BlindedSignature, error) { // mint generates promises for keys. checks lightning invoice before creating promise. return m.mint(messages, pr, keySet) } -func (m Mint) MintWithoutKeySet(messages cashu.BlindedMessages, pr string) ([]cashu.BlindedSignature, error) { +func (m *Mint) MintWithoutKeySet(messages cashu.BlindedMessages, pr string) ([]cashu.BlindedSignature, error) { // mint generates promises for keys. checks lightning invoice before creating promise. keyset, err := m.LoadKeySet(m.KeySetId) if err != nil { @@ -535,7 +529,7 @@ func (m *Mint) Melt(proofs []cashu.Proof, invoice string) (payment lightning.Pay if err != nil { return } - defer m.unsetProofsPending(proofs, &err) + defer m.unsetProofsPending(proofs) var total uint64 if err = m.verifyProofs(proofs); err != nil { @@ -574,7 +568,7 @@ func (m *Mint) Split(proofs []cashu.Proof, amount uint64, outputs []cashu.Blinde if err != nil { return nil, nil, err } - defer m.unsetProofsPending(proofs, &err) + defer m.unsetProofsPending(proofs) total := lo.SumBy[cashu.Proof](proofs, func(p cashu.Proof) uint64 { return p.Amount }) diff --git a/cmd/cashu/feni/client.go b/wallet/client.go similarity index 99% rename from cmd/cashu/feni/client.go rename to wallet/client.go index 575f73f..33d23b4 100644 --- a/cmd/cashu/feni/client.go +++ b/wallet/client.go @@ -1,4 +1,4 @@ -package feni +package wallet import ( "encoding/hex" diff --git a/wallet/config.go b/wallet/config.go new file mode 100644 index 0000000..6e13953 --- /dev/null +++ b/wallet/config.go @@ -0,0 +1,57 @@ +package wallet + +import ( + "github.com/caarlos0/env/v6" + "github.com/cashubtc/cashu-feni/lightning" + "github.com/joho/godotenv" + log "github.com/sirupsen/logrus" + "math/rand" + "os" + "path" + "time" +) + +type Config struct { + Debug bool `env:"DEBUG"` + Lightning bool `env:"LIGHTNING"` + MintServerHost string `env:"MINT_HOST"` + MintServerPort string `env:"MINT_PORT"` + Wallet string `env:"WALLET"` +} + +func (w *Wallet) defaultConfig() { + log.Infof("Loading default configuration") + w.Config = Config{ + Debug: true, + Lightning: true, + MintServerHost: "https://8333.space", + MintServerPort: "3339", + Wallet: "wallet", + } + +} +func (w *Wallet) startClientConfiguration() { + loaded := false + dirname, err := os.UserHomeDir() + if err != nil { + log.Fatal(err) + } + p := path.Join(dirname, ".cashu", ".env") + err = godotenv.Load(p) + if err != nil { + w.defaultConfig() + loaded = true + } + if !loaded { + err = env.Parse(&w.Config) + if err != nil { + w.defaultConfig() + } + } + + // initialize the default wallet (no other option selected using -w) + lightning.Config.Lightning.Enabled = w.Config.Lightning + + rand.Seed(time.Now().UnixNano()) + +} diff --git a/cmd/cashu/feni/wallet.go b/wallet/wallet.go similarity index 60% rename from cmd/cashu/feni/wallet.go rename to wallet/wallet.go index 6f7c14d..b4d35f9 100644 --- a/cmd/cashu/feni/wallet.go +++ b/wallet/wallet.go @@ -1,11 +1,14 @@ -package feni +package wallet import ( "encoding/base64" "encoding/hex" "fmt" + "github.com/caarlos0/env/v6" "math/rand" "net/url" + "os" + "path" "time" "github.com/cashubtc/cashu-feni/cashu" @@ -35,15 +38,42 @@ func Zip[T, U any](ts []T, us []U) []Pair[T, U] { return pairs } -type MintWallet struct { +type Wallet struct { // keys map[uint64]*secp256k1.PublicKey // current public keys from mint server keySets []crypto.KeySet // current keySet id from mint server. proofs []cashu.Proof currentKeySet *crypto.KeySet Client *Client + Storage db.MintStorage + Config Config } +type Option func(w *Wallet) -var Wallet MintWallet +func WithName(name string) Option { + return func(w *Wallet) { + w.Config.Wallet = name + } +} +func New(opts ...Option) *Wallet { + wallet := &Wallet{ + proofs: make([]cashu.Proof, 0), + } + wallet.startClientConfiguration() + for _, opt := range opts { + opt(wallet) + } + wallet.initializeDatabase(wallet.Config.Wallet) + + wallet.Client = &Client{Url: fmt.Sprintf("%s:%s", wallet.Config.MintServerHost, wallet.Config.MintServerPort)} + wallet.LoadDefaultMint() + + proofs, err := wallet.Storage.GetUsedProofs() + if err != nil { + return nil + } + wallet.proofs = proofs + return wallet +} // constructOutputs takes in a slice of amounts and a slice of secrets, and // constructs a MintRequest with blinded messages and a slice of private keys @@ -73,31 +103,41 @@ func constructOutputs(amounts []uint64, secrets []string) (cashu.MintRequest, [] return payloads, privateKeys } -func (w MintWallet) checkUsedSecrets(amounts []uint64, secrets []string) error { - proofs := storage.ProofsUsed(secrets) +func (w *Wallet) checkUsedSecrets(amounts []uint64, secrets []string) error { + proofs := w.Storage.ProofsUsed(secrets) if len(proofs) > 0 { return fmt.Errorf("proofs already used") } return nil } -func (w MintWallet) availableBalance() uint64 { +func (w *Wallet) AvailableBalance() uint64 { return SumProofs(w.proofs) } -func (w MintWallet) Mint(amount uint64, paymentHash string) ([]cashu.Proof, error) { +func (w *Wallet) StoreProofs(proofs []cashu.Proof) error { + for _, proof := range proofs { + w.proofs = append(w.proofs, proof) + err := w.Storage.StoreProof(proof) + if err != nil { + return err + } + } + return nil +} +func (w *Wallet) Mint(amount uint64, paymentHash string) ([]cashu.Proof, error) { split := mint.AmountSplit(amount) proofs := w.mint(split, paymentHash) if len(proofs) == 0 { return nil, fmt.Errorf("received no proofs.") } - err := storeProofs(proofs) + err := w.StoreProofs(proofs) if err != nil { return nil, err } if paymentHash != "" { - err = storage.UpdateLightningInvoice( - hash, + err = w.Storage.UpdateLightningInvoice( + paymentHash, db.UpdateInvoicePaid(true), db.UpdateInvoiceTimePaid(time.Now()), ) @@ -108,7 +148,7 @@ func (w MintWallet) Mint(amount uint64, paymentHash string) ([]cashu.Proof, erro w.proofs = append(w.proofs, proofs...) return proofs, nil } -func (w MintWallet) mint(amounts []uint64, paymentHash string) []cashu.Proof { +func (w *Wallet) mint(amounts []uint64, paymentHash string) []cashu.Proof { secrets := make([]string, 0) for range amounts { secrets = append(secrets, generateSecret()) @@ -125,7 +165,7 @@ func (w MintWallet) mint(amounts []uint64, paymentHash string) []cashu.Proof { return w.constructProofs(blindedSignatures.Promises, secrets, privateKeys) } -func (w MintWallet) constructProofs(promises []cashu.BlindedSignature, secrets []string, privateKeys []*secp256k1.PrivateKey) []cashu.Proof { +func (w *Wallet) constructProofs(promises []cashu.BlindedSignature, secrets []string, privateKeys []*secp256k1.PrivateKey) []cashu.Proof { proofs := make([]cashu.Proof, 0) for i, promise := range promises { h, err := hex.DecodeString(promise.C_) @@ -152,6 +192,12 @@ type Balance struct { Available uint64 Mint Mint } +type Mint struct { + URL string `json:"url"` + Ks []string `json:"ks"` +} +type Mints map[string]Mint + type Balances []*Balance func (b Balances) ById(id string) *Balance { @@ -162,10 +208,10 @@ func (b Balances) ById(id string) *Balance { } return nil } -func (w MintWallet) getProofsPerMintUrl() cashu.Proofs { +func (w *Wallet) getProofsPerMintUrl() cashu.Proofs { return w.proofs } -func (w MintWallet) balancePerKeySet() (Balances, error) { +func (w *Wallet) Balances() (Balances, error) { balances := make(Balances, 0) for _, proof := range w.proofs { proofBalance, foundBalance := lo.Find[*Balance](balances, func(b *Balance) bool { @@ -220,9 +266,6 @@ func generateSecrets(secret string, n int) []string { func generateSecret() string { return base64.RawURLEncoding.EncodeToString([]byte(RandStringRunes(16))) } -func getUnusedLocks(addressSplit string) ([]cashu.P2SHScript, error) { - return storage.GetScripts(addressSplit) -} var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") @@ -234,7 +277,7 @@ func RandStringRunes(n int) string { return string(b) } -func (w MintWallet) PayLightning(proofs []cashu.Proof, invoice string) ([]cashu.Proof, error) { +func (w *Wallet) PayLightning(proofs []cashu.Proof, invoice string) ([]cashu.Proof, error) { secrets := make([]string, 0) amounts := []uint64{0, 0, 0, 0} for i := 0; i < 4; i++ { @@ -247,7 +290,7 @@ func (w MintWallet) PayLightning(proofs []cashu.Proof, invoice string) ([]cashu. } if res.Paid { changeProofs := w.constructProofs(res.Change, secrets, rs) - err = invalidate(proofs) + err = w.Invalidate(proofs) if err != nil { return changeProofs, err } @@ -255,7 +298,7 @@ func (w MintWallet) PayLightning(proofs []cashu.Proof, invoice string) ([]cashu. } return nil, fmt.Errorf("could not pay invoice") } -func (w MintWallet) getKeySet(id string) (crypto.KeySet, error) { +func (w *Wallet) getKeySet(id string) (crypto.KeySet, error) { k, found := lo.Find[crypto.KeySet](w.keySets, func(k crypto.KeySet) bool { return k.Id == id }) @@ -265,7 +308,7 @@ func (w MintWallet) getKeySet(id string) (crypto.KeySet, error) { return k, nil } -func (w MintWallet) GetSpendableProofs() ([]cashu.Proof, error) { +func (w *Wallet) GetSpendableProofs() ([]cashu.Proof, error) { spendable := make([]cashu.Proof, 0) for _, proof := range lo.Filter[cashu.Proof](w.proofs, func(p cashu.Proof, i int) bool { return p.Id == w.currentKeySet.Id @@ -285,7 +328,7 @@ func (w MintWallet) GetSpendableProofs() ([]cashu.Proof, error) { return spendable, nil } -func (w MintWallet) SplitToSend(amount uint64, scndSecret string, setReserved bool) (keep []cashu.Proof, send []cashu.Proof, err error) { +func (w *Wallet) SplitToSend(amount uint64, scndSecret string, setReserved bool) (keep []cashu.Proof, send []cashu.Proof, err error) { spendableProofs, err := w.GetSpendableProofs() if err != nil { return nil, nil, err @@ -305,30 +348,20 @@ func (w MintWallet) SplitToSend(amount uint64, scndSecret string, setReserved bo } return keepProofs, SendProofs, err } -func (w MintWallet) setReserved(p []cashu.Proof, reserved bool) error { +func (w *Wallet) setReserved(p []cashu.Proof, reserved bool) error { for _, proof := range p { proof.Reserved = reserved proof.SendId = uuid.New() proof.TimeReserved = time.Now() - err := storage.StoreProof(proof) + err := w.Storage.StoreProof(proof) if err != nil { return err } } return nil } -func (w MintWallet) redeem(proofs []cashu.Proof, scndScript, scndSignature string) (keep []cashu.Proof, send []cashu.Proof, err error) { - if scndScript != "" && scndSignature != "" { - log.Infof("Unlock script: %s", scndScript) - for i := range proofs { - proofs[i].Script = &cashu.P2SHScript{ - Script: scndScript, - Signature: scndSignature} - } - } - return w.Split(proofs, SumProofs(proofs), "") -} -func (w *MintWallet) Split(proofs []cashu.Proof, amount uint64, scndSecret string) (keep []cashu.Proof, send []cashu.Proof, err error) { + +func (w *Wallet) Split(proofs []cashu.Proof, amount uint64, scndSecret string) (keep []cashu.Proof, send []cashu.Proof, err error) { if len(proofs) < 0 { return nil, nil, fmt.Errorf("no proofs provided.") } @@ -351,19 +384,19 @@ func (w *MintWallet) Split(proofs []cashu.Proof, amount uint64, scndSecret strin }) w.proofs = append(w.proofs, frstProofs...) w.proofs = append(w.proofs, scndProofs...) - err = storeProofs(append(frstProofs, scndProofs...)) + err = w.StoreProofs(append(frstProofs, scndProofs...)) if err != nil { return nil, nil, err } for _, proof := range proofs { - err = invalidateProof(proof) + err = w.invalidateProof(proof) if err != nil { return nil, nil, err } } return frstProofs, scndProofs, nil } -func (w MintWallet) split(proofs []cashu.Proof, amount uint64, scndSecret string) (keep []cashu.Proof, send []cashu.Proof, err error) { +func (w *Wallet) split(proofs []cashu.Proof, amount uint64, scndSecret string) (keep []cashu.Proof, send []cashu.Proof, err error) { total := SumProofs(proofs) frstAmt := total - amount @@ -407,3 +440,168 @@ func SumProofs(p []cashu.Proof) uint64 { } return sum } + +func (w *Wallet) Invalidate(proofs []cashu.Proof) error { + if len(proofs) == 0 { + proofs = w.proofs + } + resp, err := w.Client.Check(cashu.CheckSpendableRequest{Proofs: proofs}) + if err != nil { + return err + } + invalidatedProofs := make([]cashu.Proof, 0) + for i, spendable := range resp.Spendable { + if !spendable { + invalidatedProofs = append(invalidatedProofs, proofs[i]) + err = w.invalidateProof(proofs[i]) + if err != nil { + return err + } + } + } + invalidatedSecrets := make([]string, 0) + for _, proof := range invalidatedProofs { + invalidatedSecrets = append(invalidatedSecrets, proof.Secret) + } + w.proofs = lo.Filter[cashu.Proof](w.proofs, func(p cashu.Proof, i int) bool { + _, found := lo.Find[string](invalidatedSecrets, func(secret string) bool { + return secret == p.Secret + }) + return !found + }) + return nil +} +func (w *Wallet) invalidateProof(proof cashu.Proof) error { + err := w.Storage.DeleteProof(proof) + if err != nil { + return err + } + return w.Storage.StoreUsedProofs( + cashu.ProofsUsed{ + Secret: proof.Secret, + Amount: proof.Amount, + C: proof.C, + TimeUsed: time.Now(), + }, + ) +} + +func (w *Wallet) loadMint(keySetId string) { + /*keySet, err := Storage.GetKeySet(db.KeySetWithId(keySetId)) + if err != nil { + panic(err) + } + */ + for _, set := range w.keySets { + if set.Id == keySetId { + w.currentKeySet = &set + } + } + w.Client.Url = w.currentKeySet.MintUrl + w.LoadDefaultMint() +} +func (w *Wallet) setCurrentKeySet(keySet crypto.KeySet) { + for _, set := range w.keySets { + if set.Id == keySet.Id { + w.currentKeySet = &keySet + } + } +} +func (w *Wallet) loadPersistedKeySets() { + persistedKeySets, err := w.Storage.GetKeySet() + if err != nil { + panic(err) + } + w.keySets = persistedKeySets +} +func (w *Wallet) LoadDefaultMint() { + keySet, _ := w.persistCurrentKeysSet() + w.loadPersistedKeySets() + w.setCurrentKeySet(keySet) + k, err := w.Client.KeySets() + if err != nil { + panic(err) + } + for _, set := range k.KeySets { + if _, found := lo.Find[crypto.KeySet](w.keySets, func(k crypto.KeySet) bool { + return set == k.Id + }); !found { + err = w.checkAndPersistKeySet(set) + if err != nil { + panic(err) + } + } + } + +} + +func (w *Wallet) persistCurrentKeysSet() (crypto.KeySet, error) { + activeKeys, err := w.Client.Keys() + if err != nil { + panic(err) + } + return w.persistKeysSet(activeKeys) +} + +func (w *Wallet) persistKeysSet(keys map[uint64]*secp256k1.PublicKey) (crypto.KeySet, error) { + keySet := crypto.KeySet{MintUrl: w.Client.Url, FirstSeen: time.Now(), PublicKeys: crypto.PublicKeyList{}} + keySet.SetPublicKeyList(keys) + keySet.DeriveKeySetId() + err := w.Storage.StoreKeySet(keySet) + if err != nil { + return keySet, err + } + return keySet, nil +} + +func (w *Wallet) checkAndPersistKeySet(id string) error { + var ks []crypto.KeySet + var err error + if ks, err = w.Storage.GetKeySet(db.KeySetWithId(id)); err != nil || len(ks) == 0 { + keys, err := w.Client.KeysForKeySet(id) + if err != nil { + return err + } + k, err := w.persistKeysSet(keys) + ks = append(ks, k) + if err != nil { + return err + } + } + w.keySets = append(w.keySets, ks...) + return nil +} + +func (w *Wallet) initializeDatabase(wallet string) { + dirname, err := os.UserHomeDir() + if err != nil { + log.Fatal(err) + } + walletPath := path.Join(dirname, ".cashu", wallet) + db.Config.Database.Sqlite = &db.SqliteConfig{Path: walletPath, FileName: "wallet.sqlite3"} + err = env.Parse(&w.Config) + if err != nil { + panic(err) + } + w.Storage = db.NewSqlDatabase() + err = w.Storage.Migrate(cashu.Proof{}) + if err != nil { + panic(err) + } + err = w.Storage.Migrate(cashu.ProofsUsed{}) + if err != nil { + panic(err) + } + err = w.Storage.Migrate(crypto.KeySet{}) + if err != nil { + panic(err) + } + err = w.Storage.Migrate(cashu.P2SHScript{}) + if err != nil { + panic(err) + } + err = w.Storage.Migrate(cashu.CreateInvoice()) + if err != nil { + panic(err) + } +} diff --git a/cmd/cashu/feni/wallet_test.go b/wallet/wallet_test.go similarity index 97% rename from cmd/cashu/feni/wallet_test.go rename to wallet/wallet_test.go index 31e7110..fb21c42 100644 --- a/cmd/cashu/feni/wallet_test.go +++ b/wallet/wallet_test.go @@ -1,4 +1,4 @@ -package feni +package wallet import ( "reflect"