diff --git a/cmd/wavelet/shell.go b/cmd/wavelet/actions.go similarity index 52% rename from cmd/wavelet/shell.go rename to cmd/wavelet/actions.go index 5d853bd0..47c3826a 100644 --- a/cmd/wavelet/shell.go +++ b/cmd/wavelet/actions.go @@ -24,147 +24,17 @@ import ( "encoding/binary" "encoding/hex" "fmt" - "io" "io/ioutil" "strconv" - "strings" - "github.com/chzyer/readline" - "github.com/perlin-network/noise/skademlia" + wasm "github.com/perlin-network/life/wasm-validation" "github.com/perlin-network/wavelet" - "github.com/perlin-network/wavelet/log" "github.com/perlin-network/wavelet/sys" "github.com/pkg/errors" - "github.com/rs/zerolog" + "github.com/urfave/cli" ) -type CLI struct { - rl *readline.Instance - client *skademlia.Client - ledger *wavelet.Ledger - logger zerolog.Logger - keys *skademlia.Keypair - tree string -} - -func NewCLI(client *skademlia.Client, ledger *wavelet.Ledger, keys *skademlia.Keypair) (*CLI, error) { - completer := readline.NewPrefixCompleter( - readline.PcItem("l"), readline.PcItem("status"), - readline.PcItem("p"), readline.PcItem("pay"), - readline.PcItem("c"), readline.PcItem("call"), - readline.PcItem("f"), readline.PcItem("find"), - readline.PcItem("g"), readline.PcItem("deposit-gas"), - readline.PcItem("s"), readline.PcItem("spawn"), - readline.PcItem("ps"), readline.PcItem("place-stake"), - readline.PcItem("ws"), readline.PcItem("withdraw-stake"), - readline.PcItem("wr"), readline.PcItem("withdraw-reward"), - readline.PcItem("help"), - ) - - rl, err := readline.NewEx( - &readline.Config{ - Prompt: "\033[31m»»»\033[0m ", - AutoComplete: completer, - HistoryFile: "/tmp/readline.tmp", - InterruptPrompt: "^C", - EOFPrompt: "exit", - HistorySearchFold: true, - }, - ) - if err != nil { - return nil, err - } - - log.SetWriter(log.LoggerWavelet, log.NewConsoleWriter(rl.Stderr(), log.FilterFor(log.ModuleNode, log.ModuleNetwork, log.ModuleSync, log.ModuleConsensus, log.ModuleContract))) - - return &CLI{ - rl: rl, - client: client, - ledger: ledger, - logger: log.Node(), - tree: completer.Tree(" "), - keys: keys, - }, nil -} - -func toCMD(in string, n int) []string { - in = strings.TrimSpace(in[n:]) - if in == "" { - return []string{} - } - - return strings.Split(in, " ") -} - -func (cli *CLI) Start() { - defer func() { - _ = cli.rl.Close() - }() - - for { - line, err := cli.rl.Readline() - switch err { - case readline.ErrInterrupt: - if len(line) == 0 { - return - } else { - continue - } - case io.EOF: - return - } - - switch { - case line == "l" || line == "status": - cli.status() - case strings.HasPrefix(line, "p "): - cli.pay(toCMD(line, 2)) - case strings.HasPrefix(line, "pay "): - cli.pay(toCMD(line, 4)) - case strings.HasPrefix(line, "c "): - cli.call(toCMD(line, 2)) - case strings.HasPrefix(line, "call "): - cli.call(toCMD(line, 5)) - case strings.HasPrefix(line, "f "): - cli.find(toCMD(line, 2)) - case strings.HasPrefix(line, "find "): - cli.find(toCMD(line, 5)) - case strings.HasPrefix(line, "g "): - cli.depositGas(toCMD(line, 2)) - case strings.HasPrefix(line, "deposit-gas"): - cli.depositGas(toCMD(line, 11)) - case strings.HasPrefix(line, "s "): - cli.spawn(toCMD(line, 2)) - case strings.HasPrefix(line, "spawn "): - cli.spawn(toCMD(line, 5)) - case strings.HasPrefix(line, "ps "): - cli.placeStake(toCMD(line, 3)) - case strings.HasPrefix(line, "place-stake "): - cli.placeStake(toCMD(line, 12)) - case strings.HasPrefix(line, "ws "): - cli.withdrawStake(toCMD(line, 3)) - case strings.HasPrefix(line, "withdraw-stake "): - cli.withdrawStake(toCMD(line, 15)) - case strings.HasPrefix(line, "wr "): - cli.withdrawReward(toCMD(line, 3)) - case strings.HasPrefix(line, "withdraw-reward "): - cli.withdrawReward(toCMD(line, 16)) - case line == "": - fallthrough - case line == "help": - cli.usage() - default: - fmt.Printf("unrecognised command :'%s'\n", line) - } - } -} - -func (cli *CLI) usage() { - _, _ = io.WriteString(cli.rl.Stderr(), "commands:\n") - _, _ = io.WriteString(cli.rl.Stderr(), cli.tree) -} - -func (cli *CLI) status() { +func (cli *CLI) status(ctx *cli.Context) { preferredID := "N/A" if preferred := cli.ledger.Finalizer().Preferred(); preferred != nil { @@ -194,7 +64,9 @@ func (cli *CLI) status() { } cli.logger.Info(). - Uint8("difficulty", round.ExpectedDifficulty(sys.MinDifficulty, sys.DifficultyScaleFactor)). + Uint8("difficulty", round.ExpectedDifficulty( + sys.MinDifficulty, sys.DifficultyScaleFactor, + )). Uint64("round", round.Index). Hex("root_id", round.End.ID[:]). Uint64("height", cli.ledger.Graph().Height()). @@ -213,76 +85,32 @@ func (cli *CLI) status() { Msg("Here is the current status of your node.") } -func (cli *CLI) depositGas(cmd []string) { - if len(cmd) != 2 { - fmt.Println("deposit-gas ") - return - } - - recipient, err := hex.DecodeString(cmd[0]) - if err != nil { - cli.logger.Error().Err(err).Msg("The recipient you specified is invalid.") - return - } - - if len(recipient) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(recipient)).Msg("You have specified an invalid account ID to find.") - return - } +func (cli *CLI) pay(ctx *cli.Context) { + var cmd = ctx.Args() - amount, err := strconv.ParseUint(cmd[1], 10, 64) - if err != nil { - cli.logger.Error().Err(err).Msg("Failed to convert payment amount to a uint64.") - return - } - - var payload wavelet.Transfer - copy(payload.Recipient[:], recipient) - payload.GasDeposit = amount - - snapshot := cli.ledger.Snapshot() - - balance, _ := wavelet.ReadAccountBalance(snapshot, cli.keys.PublicKey()) - _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) - - if balance < amount+sys.TransactionFeeAmount { - cli.logger.Error().Uint64("your_balance", balance).Uint64("amount_to_send", amount).Msg("You do not have enough PERLs to deposit into the smart contract.") - return - } - - if !codeAvailable { - cli.logger.Error().Hex("recipient_id", recipient).Msg("The recipient you specified is not a smart contract.") - return - } - - tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal())) - if err != nil { - return - } - - cli.logger.Info().Msgf("Success! Your gas deposit transaction ID: %x", tx.ID) -} - -func (cli *CLI) pay(cmd []string) { - if len(cmd) != 2 { - fmt.Println("pay ") + if len(cmd) < 2 { + cli.logger.Error(). + Msg("Invalid usage: pay ") return } recipient, err := hex.DecodeString(cmd[0]) if err != nil { - cli.logger.Error().Err(err).Msg("The recipient you specified is invalid.") + cli.logger.Error().Err(err). + Msg("The recipient you specified is invalid.") return } if len(recipient) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(recipient)).Msg("You have specified an invalid account ID to find.") + cli.logger.Error().Int("length", len(recipient)). + Msg("You have specified an invalid account ID to find.") return } amount, err := strconv.ParseUint(cmd[1], 10, 64) if err != nil { - cli.logger.Error().Err(err).Msg("Failed to convert payment amount to a uint64.") + cli.logger.Error().Err(err). + Msg("Failed to convert payment amount to a uint64.") return } @@ -295,39 +123,53 @@ func (cli *CLI) pay(cmd []string) { balance, _ := wavelet.ReadAccountBalance(snapshot, cli.keys.PublicKey()) if balance < amount+sys.TransactionFeeAmount { - cli.logger.Error().Uint64("your_balance", balance).Uint64("amount_to_send", amount).Msg("You do not have enough PERLs to send.") + cli.logger.Error(). + Uint64("your_balance", balance). + Uint64("amount_to_send", amount). + Msg("You do not have enough PERLs to send.") return } - _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) + _, codeAvailable := wavelet.ReadAccountContractCode( + snapshot, payload.Recipient, + ) if codeAvailable { // Set gas limit by default to the balance the user has. payload.GasLimit = balance - amount - sys.TransactionFeeAmount payload.FuncName = []byte("on_money_received") } - tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal())) + tx, err := cli.sendTransaction(wavelet.NewTransaction( + cli.keys, sys.TagTransfer, payload.Marshal(), + )) + if err != nil { return } - cli.logger.Info().Msgf("Success! Your payment transaction ID: %x", tx.ID) + cli.logger.Info(). + Msgf("Success! Your payment transaction ID: %x", tx.ID) } -func (cli *CLI) call(cmd []string) { +func (cli *CLI) call(ctx *cli.Context) { + var cmd = ctx.Args() + if len(cmd) < 4 { - fmt.Println("call [function parameters]") + cli.logger.Error(). + Msg("Invalid usage: call [function parameters]") return } recipient, err := hex.DecodeString(cmd[0]) if err != nil { - cli.logger.Error().Err(err).Msg("The smart contract address you specified is invalid.") + cli.logger.Error().Err(err). + Msg("The smart contract address you specified is invalid.") return } if len(recipient) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(recipient)).Msg("You have specified an invalid account ID to find.") + cli.logger.Error().Int("length", len(recipient)). + Msg("You have specified an invalid account ID to find.") return } @@ -340,24 +182,30 @@ func (cli *CLI) call(cmd []string) { _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) if !codeAvailable { - cli.logger.Error().Msg("The smart contract address you specified does not belong to a smart contract.") + cli.logger.Error(). + Msg("The smart contract address you specified does not belong to a smart contract.") return } amount, err := strconv.ParseUint(cmd[1], 10, 64) if err != nil { - cli.logger.Error().Err(err).Msg("Failed to convert payment amount to a uint64.") + cli.logger.Error().Err(err). + Msg("Failed to convert payment amount to a uint64.") return } gasLimit, err := strconv.ParseUint(cmd[2], 10, 64) if err != nil { - cli.logger.Error().Err(err).Msg("Failed to convert gas limit to a uint64.") + cli.logger.Error().Err(err). + Msg("Failed to convert gas limit to a uint64.") return } if balance < amount+gasLimit { - cli.logger.Error().Uint64("your_balance", balance).Uint64("cost", amount+gasLimit).Msg("You do not have enough PERLs to pay for the costs to invoke the smart contract function you wanted.") + cli.logger.Error(). + Uint64("your_balance", balance). + Uint64("cost", amount+gasLimit). + Msg("You do not have enough PERLs to pay for the costs to invoke the smart contract function you wanted.") return } @@ -384,7 +232,8 @@ func (cli *CLI) call(cmd []string) { var val uint64 _, err := fmt.Sscanf(arg[1:], "%d", &val) if err != nil { - cli.logger.Error().Err(err).Msgf("Got an error parsing integer: %+v", arg[1:]) + cli.logger.Error().Err(err). + Msgf("Got an error parsing integer: %+v", arg[1:]) } switch arg[0] { @@ -404,30 +253,39 @@ func (cli *CLI) call(cmd []string) { buf, err := hex.DecodeString(arg[1:]) if err != nil { - cli.logger.Error().Err(err).Msgf("Cannot decode hex: %s", arg[1:]) + cli.logger.Error().Err(err). + Msgf("Cannot decode hex: %s", arg[1:]) return } params.Write(buf) default: - cli.logger.Error().Msgf("Invalid argument specified: %s", arg) + cli.logger.Error(). + Msgf("Invalid argument specified: %s", arg) return } } payload.FuncParams = params.Bytes() - tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal())) + tx, err := cli.sendTransaction(wavelet.NewTransaction( + cli.keys, sys.TagTransfer, payload.Marshal(), + )) + if err != nil { return } - cli.logger.Info().Msgf("Success! Your smart contract invocation transaction ID: %x", tx.ID) + cli.logger.Info(). + Msgf("Success! Your smart contract invocation transaction ID: %x", tx.ID) } -func (cli *CLI) find(cmd []string) { - if len(cmd) != 1 { - fmt.Println("find ") +func (cli *CLI) find(ctx *cli.Context) { + var cmd = ctx.Args() + + if len(cmd) < 1 { + cli.logger.Error(). + Msg("Invalid usage: find ") return } @@ -437,12 +295,14 @@ func (cli *CLI) find(cmd []string) { buf, err := hex.DecodeString(address) if err != nil { - cli.logger.Error().Err(err).Msg("Cannot decode address") + cli.logger.Error().Err(err). + Msg("Cannot decode address") return } if len(buf) != wavelet.SizeTransactionID && len(buf) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(buf)).Msg("You have specified an invalid transaction/account ID to find.") + cli.logger.Error().Int("length", len(buf)). + Msg("You have specified an invalid transaction/account ID to find.") return } @@ -498,9 +358,12 @@ func (cli *CLI) find(cmd []string) { } } -func (cli *CLI) spawn(cmd []string) { - if len(cmd) != 1 { - fmt.Println("spawn ") +func (cli *CLI) spawn(ctx *cli.Context) { + var cmd = ctx.Args() + + if len(cmd) < 1 { + cli.logger.Error(). + Msg("Invalid usage: spawn ") return } @@ -513,6 +376,14 @@ func (cli *CLI) spawn(cmd []string) { return } + if err := wasm.GetValidator().ValidateWasm(code); err != nil { + cli.logger.Error(). + Err(err). + Str("path", cmd[0]). + Msg("Invalid wasm") + return + } + payload := wavelet.Contract{ GasLimit: 100000000, Code: code, @@ -526,15 +397,91 @@ func (cli *CLI) spawn(cmd []string) { cli.logger.Info().Msgf("Success! Your smart contracts ID: %x", tx.ID) } -func (cli *CLI) placeStake(cmd []string) { - if len(cmd) != 1 { - fmt.Println("place-stake ") +func (cli *CLI) depositGas(ctx *cli.Context) { + var cmd = ctx.Args() + + if len(cmd) < 2 { + cli.logger.Error(). + Msg("Invalid usage: deposit-gas ") + return + } + + // Get the recipient ID + recipient, err := hex.DecodeString(cmd[0]) + if err != nil { + cli.logger.Error().Err(err). + Msg("The recipient you specified is invalid.") return } + // Check if the ID is actually invalid by length + if len(recipient) != wavelet.SizeAccountID { + cli.logger.Error().Int("length", len(recipient)). + Msg("You have specified an invalid account ID to find.") + return + } + + // Parse the gas amount amount, err := strconv.ParseUint(cmd[1], 10, 64) if err != nil { - cli.logger.Error().Err(err).Msg("Failed to convert staking amount to a uint64.") + cli.logger.Error().Err(err). + Msg("Failed to convert payment amount to an uint64.") + return + } + + // Make a new payload, copy the recipient over and assign the amount + var payload wavelet.Transfer + copy(payload.Recipient[:], recipient) + payload.GasDeposit = amount + + // Get snapshot + snapshot := cli.ledger.Snapshot() + + // Get balance and check if recipient is a smart contract + balance, _ := wavelet.ReadAccountBalance(snapshot, cli.keys.PublicKey()) + _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) + + // Check balance + if balance < amount+sys.TransactionFeeAmount { + cli.logger.Error(). + Uint64("your_balance", balance). + Uint64("amount_to_send", amount). + Msg("You do not have enough PERLs to deposit into the smart contract.") + return + } + + // The recipient is not a smart contract + if !codeAvailable { + cli.logger.Error().Hex("recipient_id", recipient). + Msg("The recipient you specified is not a smart contract.") + return + } + + tx, err := cli.sendTransaction( + wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal()), + ) + + if err != nil { + return + } + + cli.logger.Info(). + Msgf("Success! Your gas deposit transaction ID: %x", tx.ID) +} + +func (cli *CLI) placeStake(ctx *cli.Context) { + var cmd = ctx.Args() + + if len(cmd) < 1 { + cli.logger.Error(). + Msg("Invalid usage: place-stake ") + return + } + + amount, err := strconv.ParseUint(cmd[0], 10, 64) + if err != nil { + cli.logger.Error().Err(err). + Msg("Failed to convert staking amount to a uint64.") return } @@ -543,7 +490,10 @@ func (cli *CLI) placeStake(cmd []string) { Amount: amount, } - tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagStake, payload.Marshal())) + tx, err := cli.sendTransaction(wavelet.NewTransaction( + cli.keys, sys.TagStake, payload.Marshal(), + )) + if err != nil { return } @@ -552,41 +502,55 @@ func (cli *CLI) placeStake(cmd []string) { Msgf("Success! Your stake placement transaction ID: %x", tx.ID) } -func (cli *CLI) withdrawStake(cmd []string) { - if len(cmd) != 1 { - fmt.Println("withdraw-stake ") +func (cli *CLI) withdrawStake(ctx *cli.Context) { + var cmd = ctx.Args() + + if len(cmd) < 1 { + cli.logger.Error(). + Msg("Invalid usage: withdraw-stake ") return } - amount, err := strconv.ParseUint(cmd[1], 10, 64) + amount, err := strconv.ParseUint(cmd[0], 10, 64) if err != nil { - cli.logger.Error().Err(err).Msg("Failed to convert staking amount to a uint64.") + cli.logger.Error().Err(err). + Msg("Failed to convert withdraw amount to an uint64.") return } - payload := wavelet.Stake{ - Opcode: sys.WithdrawStake, - Amount: amount, - } + var intBuf [8]byte + payload := bytes.NewBuffer(nil) + payload.WriteByte(sys.WithdrawStake) + binary.LittleEndian.PutUint64(intBuf[:8], uint64(amount)) + payload.Write(intBuf[:8]) + + tx, err := cli.sendTransaction(wavelet.NewTransaction( + cli.keys, sys.TagStake, payload.Bytes(), + )) - tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagStake, payload.Marshal())) if err != nil { return } + txID := hex.EncodeToString(tx.ID[:]) + cli.logger.Info(). - Msgf("Success! Your stake withdrawal transaction ID: %x", tx.ID) + Msg("Success! Your stake withdrawal transaction ID: " + txID) } -func (cli *CLI) withdrawReward(cmd []string) { - if len(cmd) != 1 { - fmt.Println("withdraw-reward ") +func (cli *CLI) withdrawReward(ctx *cli.Context) { + var cmd = ctx.Args() + + if len(cmd) < 1 { + cli.logger.Error(). + Msg("Invalid usage: withdraw-reward ") return } amount, err := strconv.ParseUint(cmd[0], 10, 64) if err != nil { - cli.logger.Error().Err(err).Msg("Failed to convert withdraw amount to an uint64.") + cli.logger.Error().Err(err). + Msg("Failed to convert withdraw amount to an uint64.") return } @@ -595,7 +559,10 @@ func (cli *CLI) withdrawReward(cmd []string) { Amount: amount, } - tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagStake, payload.Marshal())) + tx, err := cli.sendTransaction(wavelet.NewTransaction( + cli.keys, sys.TagStake, payload.Marshal(), + )) + if err != nil { return } @@ -605,15 +572,19 @@ func (cli *CLI) withdrawReward(cmd []string) { } func (cli *CLI) sendTransaction(tx wavelet.Transaction) (wavelet.Transaction, error) { - tx = wavelet.AttachSenderToTransaction(cli.keys, tx, cli.ledger.Graph().FindEligibleParents()...) + tx = wavelet.AttachSenderToTransaction( + cli.keys, tx, cli.ledger.Graph().FindEligibleParents()..., + ) - if err := cli.ledger.AddTransaction(tx); err != nil && errors.Cause(err) != wavelet.ErrMissingParents { - cli.logger. - Err(err). - Hex("tx_id", tx.ID[:]). - Msg("Failed to create your transaction.") + if err := cli.ledger.AddTransaction(tx); err != nil { + if errors.Cause(err) != wavelet.ErrMissingParents { + cli.logger. + Err(err). + Hex("tx_id", tx.ID[:]). + Msg("Failed to create your transaction.") - return tx, err + return tx, err + } } return tx, nil diff --git a/cmd/wavelet/cli.go b/cmd/wavelet/cli.go new file mode 100644 index 00000000..2ed6ff24 --- /dev/null +++ b/cmd/wavelet/cli.go @@ -0,0 +1,244 @@ +// Copyright (c) 2019 Perlin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package main + +import ( + "encoding/csv" + "fmt" + "io" + "strings" + "text/tabwriter" + + "github.com/benpye/readline" + "github.com/perlin-network/noise/skademlia" + "github.com/perlin-network/wavelet" + "github.com/perlin-network/wavelet/log" + "github.com/rs/zerolog" + "github.com/urfave/cli" +) + +const ( + vtRed = "\033[31m" + vtReset = "\033[39m" + prompt = "»»»" +) + +type CLI struct { + app *cli.App + rl *readline.Instance + client *skademlia.Client + ledger *wavelet.Ledger + logger zerolog.Logger + keys *skademlia.Keypair + + completion []string +} + +func NewCLI(client *skademlia.Client, ledger *wavelet.Ledger, keys *skademlia.Keypair) (*CLI, error) { + c := &CLI{ + client: client, + ledger: ledger, + logger: log.Node(), + keys: keys, + app: cli.NewApp(), + } + + c.app.Name = "wavelet" + c.app.HideVersion = true + c.app.UsageText = "command [arguments...]" + c.app.CommandNotFound = func(ctx *cli.Context, s string) { + c.logger.Error(). + Msg("Unknown command: " + s) + } + + // List of commands and their actions + c.app.Commands = []cli.Command{ + { + Name: "status", + Aliases: []string{"l"}, + Action: a(c.status), + Description: "print out information about your node", + }, + { + Name: "pay", + Aliases: []string{"p"}, + Action: a(c.pay), + Description: "pay the address an amount of PERLs", + }, + { + Name: "call", + Aliases: []string{"c"}, + Action: a(c.call), + Description: "invoke a function on a smart contract", + }, + { + Name: "find", + Aliases: []string{"f"}, + Action: a(c.find), + Description: "search for any wallet/smart contract/transaction", + }, + { + Name: "spawn", + Aliases: []string{"s"}, + Action: a(c.spawn), + Description: "test deploy a smart contract", + }, + { + Name: "deposit-gas", + Aliases: []string{"g"}, + Action: a(c.depositGas), + Description: "deposit gas to a smart contract", + }, + { + Name: "place-stake", + Aliases: []string{"ps"}, + Action: a(c.placeStake), + Description: "deposit a stake of PERLs into the network", + }, + { + Name: "withdraw-stake", + Aliases: []string{"ws"}, + Action: a(c.withdrawStake), + Description: "withdraw stake and diminish voting power", + }, + { + Name: "withdraw-reward", + Aliases: []string{"wr"}, + Action: a(c.withdrawReward), + Description: "withdraw rewards into PERLs", + }, + { + Name: "exit", + Aliases: []string{"quit", ":q"}, + Action: a(c.exit), + }, + } + + // Generate the help message + s := strings.Builder{} + s.WriteString("Commands:\n") + w := tabwriter.NewWriter(&s, 0, 0, 1, ' ', 0) + + for _, c := range c.app.VisibleCommands() { + fmt.Fprintf(w, + " %s (%s) %s\t%s\n", + c.Name, strings.Join(c.Aliases, ", "), c.Usage, + c.Description, + ) + } + + w.Flush() + c.app.CustomAppHelpTemplate = s.String() + + // Add in autocompletion + var completers = make( + []readline.PrefixCompleterInterface, + 0, len(c.app.Commands)*2+1, + ) + + for _, cmd := range c.app.Commands { + switch cmd.Name { + case "spawn": + commandAddCompleter(&completers, cmd, + c.getPathCompleter()) + default: + commandAddCompleter(&completers, cmd, + c.getCompleter()) + } + } + + var completer = readline.NewPrefixCompleter(completers...) + + // Make a new readline struct + rl, err := readline.NewEx(&readline.Config{ + Prompt: vtRed + prompt + vtReset + " ", + AutoComplete: completer, + HistoryFile: "/tmp/wavelet-history.tmp", + InterruptPrompt: "^C", + EOFPrompt: "exit", + HistorySearchFold: true, + }) + + if err != nil { + return nil, err + } + + c.rl = rl + + log.SetWriter( + log.LoggerWavelet, + log.NewConsoleWriter(rl.Stdout(), log.FilterFor( + log.ModuleNode, + log.ModuleNetwork, + log.ModuleSync, + log.ModuleConsensus, + log.ModuleContract, + )), + ) + + return c, nil +} + +func (cli *CLI) Start() { +ReadLoop: + for { + line, err := cli.rl.Readline() + switch err { + case readline.ErrInterrupt: + if len(line) == 0 { + break ReadLoop + } + + continue ReadLoop + + case io.EOF: + break ReadLoop + } + + r := csv.NewReader(strings.NewReader(line)) + r.Comma = ' ' + + s, err := r.Read() + if err != nil { + s = strings.Fields(line) + } + + // Add an app name as $0 + s = append([]string{cli.app.Name}, s...) + + if err := cli.app.Run(s); err != nil { + cli.logger.Error().Err(err). + Msg("Failed to run command.") + } + } + + cli.rl.Close() +} + +func (cli *CLI) exit(ctx *cli.Context) { + cli.rl.Close() +} + +func a(f func(*cli.Context)) func(*cli.Context) error { + return func(ctx *cli.Context) error { + f(ctx) + return nil + } +} diff --git a/cmd/wavelet/completion.go b/cmd/wavelet/completion.go new file mode 100644 index 00000000..7e11d382 --- /dev/null +++ b/cmd/wavelet/completion.go @@ -0,0 +1,115 @@ +// Copyright (c) 2019 Perlin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package main + +import ( + "os" + "path/filepath" + "strings" + + "github.com/benpye/readline" + "github.com/urfave/cli" +) + +func (cli *CLI) getCompleter() *readline.PrefixCompleter { + return readline.PcItemDynamic(func(line string) []string { + f := strings.Split(line, " ") + if len(f) < 2 { + return nil + } + + text := f[len(f)-1] + + return cli.ledger.Find(text, 10) + }) +} + +type PathCompleter struct { + *readline.PrefixCompleter +} + +func (p *PathCompleter) GetDynamicNames(line []rune) [][]rune { + var path string + words := strings.Split(string(line), " ") + if len(words) > 1 && words[1] != "" { // has some file + path = filepath.Dir(strings.Join(words[1:], " ")) + } else { + path = "." + } + + f, err := os.Open(path) + if err != nil { + return nil + } + + defer f.Close() + + files, err := f.Readdir(-1) + if err != nil { + return nil + } + + names := make([][]rune, 0, len(files)) + + for _, f := range files { + filename := filepath.Join(path, f.Name()) + if f.IsDir() { + filename += "/" + } else { + filename += " " + } + + names = append(names, []rune(filename)) + } + + return names +} + +func (cli *CLI) getPathCompleter() readline.PrefixCompleterInterface { + return &PathCompleter{ + PrefixCompleter: &readline.PrefixCompleter{ + Callback: func(string) []string { return nil }, + Dynamic: true, + Children: nil, + }, + } +} + +func joinFolder(fs []string) (p string) { + for _, f := range fs { + p += f + "/" + } + + return +} + +func commandAddCompleter(completers *[]readline.PrefixCompleterInterface, + cmd cli.Command, completer readline.PrefixCompleterInterface) { + + *completers = append(*completers, readline.PcItem( + cmd.Name, completer, + )) + + for _, alias := range cmd.Aliases { + *completers = append(*completers, readline.PcItem( + alias, completer, + )) + } +} diff --git a/cmd/wavelet/main.go b/cmd/wavelet/main.go index 46c2cadf..c53e48b8 100644 --- a/cmd/wavelet/main.go +++ b/cmd/wavelet/main.go @@ -61,7 +61,17 @@ type Config struct { } func main() { - log.SetWriter(log.LoggerWavelet, log.NewConsoleWriter(nil, log.FilterFor(log.ModuleNode, log.ModuleNetwork, log.ModuleSync, log.ModuleConsensus, log.ModuleContract))) + log.SetWriter( + log.LoggerWavelet, + log.NewConsoleWriter(nil, log.FilterFor( + log.ModuleNode, + log.ModuleNetwork, + log.ModuleSync, + log.ModuleConsensus, + log.ModuleContract, + )), + ) + logger := log.Node() app := cli.NewApp() @@ -166,13 +176,15 @@ func main() { } // apply the toml before processing the flags - app.Before = altsrc.InitInputSourceWithContext(app.Flags, func(c *cli.Context) (altsrc.InputSourceContext, error) { - filePath := c.String("config") - if len(filePath) > 0 { - return altsrc.NewTomlSourceFromFile(filePath) - } - return &altsrc.MapInputSource{}, nil - }) + app.Before = altsrc.InitInputSourceWithContext( + app.Flags, func(c *cli.Context) (altsrc.InputSourceContext, error) { + filePath := c.String("config") + if len(filePath) > 0 { + return altsrc.NewTomlSourceFromFile(filePath) + } + return &altsrc.MapInputSource{}, nil + }, + ) cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("Version: %s\n", c.App.Version) @@ -216,9 +228,9 @@ func main() { sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) - err := app.Run(os.Args) - if err != nil { - logger.Fatal().Err(err).Msg("Failed to parse configuration/command-line arguments.") + if err := app.Run(os.Args); err != nil { + logger.Fatal().Err(err). + Msg("Failed to parse configuration/command-line arguments.") } } diff --git a/go.mod b/go.mod index 01454126..327fb886 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,11 @@ go 1.12 replace github.com/go-interpreter/wagon => github.com/perlin-network/wagon v0.3.1-0.20180825141017-f8cb99b55a39 require ( + github.com/armon/go-radix v1.0.0 + github.com/benpye/readline v0.0.0-20181117181432-5ff4ccac79cf github.com/buaazp/fasthttprouter v0.1.1 github.com/chzyer/logex v1.1.10 // indirect - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect - github.com/dghubble/trie v0.0.0-20190512033633-6d8e3fa705df github.com/fasthttp/websocket v1.4.0 github.com/gogo/protobuf v1.2.1 github.com/golang/snappy v0.0.1 @@ -23,6 +23,7 @@ require ( github.com/rs/zerolog v1.14.3 github.com/stretchr/testify v1.3.0 github.com/syndtr/goleveldb v1.0.0 + github.com/urfave/cli v1.21.0 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.3.0 github.com/valyala/fastjson v1.4.1 diff --git a/go.sum b/go.sum index dde954e9..65761c4b 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,20 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/benpye/readline v0.0.0-20181117181432-5ff4ccac79cf h1:JGVL1bvO7BMh81Kgsfr0uhbt4DIAxaitHzrCpjaMUwg= +github.com/benpye/readline v0.0.0-20181117181432-5ff4ccac79cf/go.mod h1:zLHWwM4VYXc1JNufyVuuP3tZkT5X9vopBY1w0EaMBsI= github.com/buaazp/fasthttprouter v0.1.1 h1:4oAnN0C3xZjylvZJdP35cxfclyn4TYkW6Y+DSvS+h8Q= github.com/buaazp/fasthttprouter v0.1.1/go.mod h1:h/Ap5oRVLeItGKTVBb+heQPks+HdIUtGmI4H5WCYijM= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dghubble/trie v0.0.0-20190512033633-6d8e3fa705df h1:WRQekGjYIb3oD1ofBVwBa7+0S+2XtUCefOiFCow9/Cw= -github.com/dghubble/trie v0.0.0-20190512033633-6d8e3fa705df/go.mod h1:P5ymVhkUtwRIkYn2IuBeuVezlrsshMKWQJymph3GOp8= github.com/fasthttp/websocket v1.4.0 h1:hWw+gsVLA82cQFDF/vzydHjOedj1Oo00T/uKk+J5kcs= github.com/fasthttp/websocket v1.4.0/go.mod h1:4nypHRMj3oQPKA3/LllCXx0F0L5VyVKlMgo3lgbbwBM= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= @@ -75,6 +75,8 @@ github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFd github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE= +github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.3.0 h1:++0WUtakkqBuHHY5JRFFl6O44I03XLBqxNnrBX0yH7Y= @@ -131,4 +133,6 @@ gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/graph.go b/graph.go index 5883f5ae..03dd6553 100644 --- a/graph.go +++ b/graph.go @@ -539,6 +539,10 @@ func (g *Graph) updateGraph(tx *Transaction) error { g.metrics.receivedTX.Mark(int64(tx.LogicalUnits())) } + if g.indexer != nil { + g.indexer.Index(hex.EncodeToString(tx.ID[:])) + } + for _, childID := range g.children[tx.ID] { if _, incomplete := g.incomplete[childID]; !incomplete { continue diff --git a/index.go b/index.go index 83440ae9..0dd62e75 100644 --- a/index.go +++ b/index.go @@ -20,63 +20,57 @@ package wavelet import ( - "github.com/dghubble/trie" - "io" - "strings" - "sync" + "github.com/armon/go-radix" ) // Indexer indexes all transaction IDs into a single trie for the // purposes of suiting the needs of implementing autocomplete // related components. type Indexer struct { - sync.RWMutex - index *trie.PathTrie + *radix.Tree } // NewIndexer instantiates trie indices for indexing complete // transactions by their ID. func NewIndexer() *Indexer { - return &Indexer{index: trie.NewPathTrie()} + return &Indexer{ + radix.New(), + } } // Index indexes a single hex-encoded transaction ID. This // method is safe to call concurrently. func (m *Indexer) Index(id string) { - m.Lock() - m.index.Put(id, struct{}{}) - m.Unlock() + m.Insert(id, nil) } // Remove un-indexes a single hex-encoded transaction ID. This // method is safe to call concurrently. func (m *Indexer) Remove(id string) { - m.Lock() - m.index.Delete(id) - m.Unlock() + m.Delete(id) } // Find searches through complete transaction indices for a specified // query string. All indices that queried are in the form of tries. -func (m *Indexer) Find(query string, count int) []string { - results := make([]string, 0, count) +func (m *Indexer) Find(query string, max int) (results []string) { + if max > 0 { + results = make([]string, 0, max) + } - m.RLock() - defer m.RUnlock() - - _ = m.index.Walk(func(key string, _ interface{}) error { - if len(results) >= count { - return io.EOF - } - - if !strings.HasPrefix(key, query) { - return io.EOF + cb := func(a string, _ interface{}) bool { + if max > 0 && len(results) >= max { + return false } - results = append(results, key) + results = append(results, a) + return true + } - return nil - }) + if query != "" { + m.WalkPrefix(query, cb) + } else { + m.Walk(cb) + } return results } diff --git a/ledger.go b/ledger.go index 9f56ec31..361cd6a6 100644 --- a/ledger.go +++ b/ledger.go @@ -25,6 +25,11 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "math/rand" + "strings" + "sync" + "time" + "github.com/perlin-network/noise" "github.com/perlin-network/noise/skademlia" "github.com/perlin-network/wavelet/avl" @@ -36,10 +41,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/connectivity" "google.golang.org/grpc/peer" - "math/rand" - "strings" - "sync" - "time" ) type Ledger struct { @@ -178,10 +179,13 @@ func (l *Ledger) AddTransaction(tx Transaction) error { // Find searches through complete transaction and account indices for a specified // query string. All indices that queried are in the form of tries. It is safe // to call this method concurrently. -func (l *Ledger) Find(query string, count int) []string { +func (l *Ledger) Find(query string, max int) (results []string) { var err error - results := make([]string, 0, count) + if max > 0 { + results = make([]string, 0, max) + } + prefix := []byte(query) if len(query)%2 == 1 { // Cut off a single character. @@ -201,7 +205,7 @@ func (l *Ledger) Find(query string, count int) []string { return false } - if len(results) >= count { + if max > 0 && len(results) >= max { return false } @@ -209,7 +213,12 @@ func (l *Ledger) Find(query string, count int) []string { return true }) - return append(results, l.indexer.Find(query, count-len(results))...) + var count = -1 + if max > 0 { + count = max - len(results) + } + + return append(results, l.indexer.Find(query, count)...) } // PushSendQuota permits one token into this nodes send quota bucket every millisecond