From 911be78cc7ac208050f9fcb515424f48f22b5dcb Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 28 Aug 2020 12:11:19 +0300 Subject: [PATCH 01/20] cli: use Writer from app instead of Stdout It is useful in tests. --- cli/server/server.go | 6 +-- cli/smartcontract/smart_contract.go | 16 +++---- cli/util/convert.go | 2 +- cli/wallet/multisig.go | 13 ++--- cli/wallet/nep5.go | 73 +++++++++++++++-------------- cli/wallet/validator.go | 12 ++--- cli/wallet/wallet.go | 67 +++++++++++++------------- 7 files changed, 96 insertions(+), 93 deletions(-) diff --git a/cli/server/server.go b/cli/server/server.go index 3f7170a8b..ed5840d72 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -357,9 +357,9 @@ func startServer(ctx *cli.Context) error { go serv.Start(errChan) go rpcServer.Start(errChan) - fmt.Println(logo()) - fmt.Println(serv.UserAgent) - fmt.Println() + fmt.Fprintln(ctx.App.Writer, logo()) + fmt.Fprintln(ctx.App.Writer, serv.UserAgent) + fmt.Fprintln(ctx.App.Writer) var shutdownErr error Main: diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index c4cca1cc5..0410e87d7 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -366,7 +366,7 @@ func initSmartContract(ctx *cli.Context) error { return cli.NewExitError(err, 1) } - fmt.Printf("Successfully initialized smart contract [%s]\n", contractName) + fmt.Fprintf(ctx.App.Writer, "Successfully initialized smart contract [%s]\n", contractName) return nil } @@ -405,7 +405,7 @@ func contractCompile(ctx *cli.Context) error { return cli.NewExitError(err, 1) } if ctx.Bool("verbose") { - fmt.Println(hex.EncodeToString(result)) + fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(result)) } return nil @@ -495,14 +495,14 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) } - fmt.Printf("Sent invocation transaction %s\n", txHash.StringLE()) + fmt.Fprintf(ctx.App.Writer, "Sent invocation transaction %s\n", txHash.StringLE()) } else { b, err := json.MarshalIndent(resp, "", " ") if err != nil { return cli.NewExitError(err, 1) } - fmt.Println(string(b)) + fmt.Fprintln(ctx.App.Writer, string(b)) } return nil @@ -599,7 +599,7 @@ func testInvokeScript(ctx *cli.Context) error { return cli.NewExitError(err, 1) } - fmt.Println(string(b)) + fmt.Fprintln(ctx.App.Writer, string(b)) return nil } @@ -680,9 +680,9 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) } - fmt.Printf("Enter account %s password > ", address.Uint160ToString(addr)) + fmt.Fprintf(ctx.App.Writer, "Enter account %s password > ", address.Uint160ToString(addr)) rawPass, err := terminal.ReadPassword(syscall.Stdin) - fmt.Println() + fmt.Fprintln(ctx.App.Writer) if err != nil { return nil, cli.NewExitError(err, 1) } @@ -751,7 +751,7 @@ func contractDeploy(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) } - fmt.Printf("Sent deployment transaction %s for contract %s\n", txHash.StringLE(), nefFile.Header.ScriptHash.StringLE()) + fmt.Fprintf(ctx.App.Writer, "Sent deployment transaction %s for contract %s\n", txHash.StringLE(), nefFile.Header.ScriptHash.StringLE()) return nil } diff --git a/cli/util/convert.go b/cli/util/convert.go index 731db8562..aecc98b43 100644 --- a/cli/util/convert.go +++ b/cli/util/convert.go @@ -33,6 +33,6 @@ func handleParse(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - fmt.Print(res) + fmt.Fprint(ctx.App.Writer, res) return nil } diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go index 3bcd47828..87bd2dffc 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -3,6 +3,7 @@ package wallet import ( "encoding/json" "fmt" + "io" "io/ioutil" "github.com/nspcc-dev/neo-go/cli/options" @@ -50,7 +51,7 @@ func signMultisig(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("invalid address: %w", err), 1) } - acc, err := getDecryptedAccount(wall, sh) + acc, err := getDecryptedAccount(ctx, wall, sh) if err != nil { return cli.NewExitError(err, 1) } @@ -59,7 +60,7 @@ func signMultisig(ctx *cli.Context) error { if !ok { return cli.NewExitError("verifiable item is not a transaction", 1) } - printTxInfo(tx) + printTxInfo(ctx.App.Writer, tx) priv := acc.PrivateKey() sign := priv.Sign(tx.GetSignedPart()) @@ -86,11 +87,11 @@ func signMultisig(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - fmt.Println(res.StringLE()) + fmt.Fprintln(ctx.App.Writer, res.StringLE()) return nil } - fmt.Println(tx.Hash().StringLE()) + fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) return nil } @@ -116,6 +117,6 @@ func writeParameterContext(c *context.ParameterContext, filename string) error { return nil } -func printTxInfo(t *transaction.Transaction) { - fmt.Printf("Hash: %s\n", t.Hash().StringLE()) +func printTxInfo(w io.Writer, t *transaction.Transaction) { + fmt.Fprintf(w, "Hash: %s\n", t.Hash().StringLE()) } diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index 641d13696..2f7397f2f 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -162,9 +162,9 @@ func getNEP5Balance(ctx *cli.Context) error { var token *wallet.Token name := ctx.String("token") if name != "" { - token, err = getMatchingToken(wall, name) + token, err = getMatchingToken(ctx, wall, name) if err != nil { - token, err = getMatchingTokenRPC(c, addrHash, name) + token, err = getMatchingTokenRPC(ctx, c, addrHash, name) if err != nil { return cli.NewExitError(err, 1) } @@ -181,26 +181,26 @@ func getNEP5Balance(ctx *cli.Context) error { if name != "" && !token.Hash.Equals(asset) { continue } - fmt.Printf("TokenHash: %s\n", asset.StringLE()) - fmt.Printf("\tAmount : %s\n", balances.Balances[i].Amount) - fmt.Printf("\tUpdated: %d\n", balances.Balances[i].LastUpdated) + fmt.Fprintf(ctx.App.Writer, "TokenHash: %s\n", asset.StringLE()) + fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", balances.Balances[i].Amount) + fmt.Fprintf(ctx.App.Writer, "\tUpdated: %d\n", balances.Balances[i].LastUpdated) } return nil } -func getMatchingToken(w *wallet.Wallet, name string) (*wallet.Token, error) { +func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string) (*wallet.Token, error) { switch strings.ToLower(name) { case "neo": return neoToken, nil case "gas": return gasToken, nil } - return getMatchingTokenAux(func(i int) *wallet.Token { + return getMatchingTokenAux(ctx, func(i int) *wallet.Token { return w.Extra.Tokens[i] }, len(w.Extra.Tokens), name) } -func getMatchingTokenRPC(c *client.Client, addr util.Uint160, name string) (*wallet.Token, error) { +func getMatchingTokenRPC(ctx *cli.Context, c *client.Client, addr util.Uint160, name string) (*wallet.Token, error) { bs, err := c.GetNEP5Balances(addr) if err != nil { return nil, err @@ -209,18 +209,18 @@ func getMatchingTokenRPC(c *client.Client, addr util.Uint160, name string) (*wal t, _ := c.NEP5TokenInfo(bs.Balances[i].Asset) return t } - return getMatchingTokenAux(get, len(bs.Balances), name) + return getMatchingTokenAux(ctx, get, len(bs.Balances), name) } -func getMatchingTokenAux(get func(i int) *wallet.Token, n int, name string) (*wallet.Token, error) { +func getMatchingTokenAux(ctx *cli.Context, get func(i int) *wallet.Token, n int, name string) (*wallet.Token, error) { var token *wallet.Token var count int for i := 0; i < n; i++ { t := get(i) if t != nil && (t.Name == name || t.Symbol == name || t.Address() == name || t.Hash.StringLE() == name) { if count == 1 { - printTokenInfo(token) - printTokenInfo(t) + printTokenInfo(ctx, token) + printTokenInfo(ctx, t) return nil, errors.New("multiple matching tokens found") } count++ @@ -247,7 +247,7 @@ func importNEP5Token(ctx *cli.Context) error { for _, t := range wall.Extra.Tokens { if t.Hash.Equals(tokenHash) { - printTokenInfo(t) + printTokenInfo(ctx, t) return cli.NewExitError("token already exists", 1) } } @@ -269,16 +269,17 @@ func importNEP5Token(ctx *cli.Context) error { if err := wall.Save(); err != nil { return cli.NewExitError(err, 1) } - printTokenInfo(tok) + printTokenInfo(ctx, tok) return nil } -func printTokenInfo(tok *wallet.Token) { - fmt.Printf("Name:\t%s\n", tok.Name) - fmt.Printf("Symbol:\t%s\n", tok.Symbol) - fmt.Printf("Hash:\t%s\n", tok.Hash.StringLE()) - fmt.Printf("Decimals: %d\n", tok.Decimals) - fmt.Printf("Address: %s\n", tok.Address()) +func printTokenInfo(ctx *cli.Context, tok *wallet.Token) { + w := ctx.App.Writer + fmt.Fprintf(w, "Name:\t%s\n", tok.Name) + fmt.Fprintf(w, "Symbol:\t%s\n", tok.Symbol) + fmt.Fprintf(w, "Hash:\t%s\n", tok.Hash.StringLE()) + fmt.Fprintf(w, "Decimals: %d\n", tok.Decimals) + fmt.Fprintf(w, "Address: %s\n", tok.Address()) } func printNEP5Info(ctx *cli.Context) error { @@ -289,19 +290,19 @@ func printNEP5Info(ctx *cli.Context) error { defer wall.Close() if name := ctx.String("token"); name != "" { - token, err := getMatchingToken(wall, name) + token, err := getMatchingToken(ctx, wall, name) if err != nil { return cli.NewExitError(err, 1) } - printTokenInfo(token) + printTokenInfo(ctx, token) return nil } for i, t := range wall.Extra.Tokens { if i > 0 { - fmt.Println() + fmt.Fprintln(ctx.App.Writer) } - printTokenInfo(t) + printTokenInfo(ctx, t) } return nil } @@ -317,12 +318,12 @@ func removeNEP5Token(ctx *cli.Context) error { if name == "" { return cli.NewExitError("token must be specified", 1) } - token, err := getMatchingToken(wall, name) + token, err := getMatchingToken(ctx, wall, name) if err != nil { return cli.NewExitError(err, 1) } if !ctx.Bool("force") { - if ok := askForConsent(); !ok { + if ok := askForConsent(ctx.App.Writer); !ok { return nil } } @@ -343,7 +344,7 @@ func multiTransferNEP5(ctx *cli.Context) error { fromFlag := ctx.Generic("from").(*flags.Address) from := fromFlag.Uint160() - acc, err := getDecryptedAccount(wall, from) + acc, err := getDecryptedAccount(ctx, wall, from) if err != nil { return cli.NewExitError(err, 1) } @@ -369,10 +370,10 @@ func multiTransferNEP5(ctx *cli.Context) error { } token, ok := cache[ss[0]] if !ok { - token, err = getMatchingToken(wall, ss[0]) + token, err = getMatchingToken(ctx, wall, ss[0]) if err != nil { - fmt.Println("Can't find matching token in the wallet. Querying RPC-node for balances.") - token, err = getMatchingTokenRPC(c, from, ctx.String("token")) + fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.") + token, err = getMatchingTokenRPC(ctx, c, from, ctx.String("token")) if err != nil { return cli.NewExitError(err, 1) } @@ -406,7 +407,7 @@ func transferNEP5(ctx *cli.Context) error { fromFlag := ctx.Generic("from").(*flags.Address) from := fromFlag.Uint160() - acc, err := getDecryptedAccount(wall, from) + acc, err := getDecryptedAccount(ctx, wall, from) if err != nil { return cli.NewExitError(err, 1) } @@ -421,10 +422,10 @@ func transferNEP5(ctx *cli.Context) error { toFlag := ctx.Generic("to").(*flags.Address) to := toFlag.Uint160() - token, err := getMatchingToken(wall, ctx.String("token")) + token, err := getMatchingToken(ctx, wall, ctx.String("token")) if err != nil { - fmt.Println("Can't find matching token in the wallet. Querying RPC-node for balances.") - token, err = getMatchingTokenRPC(c, from, ctx.String("token")) + fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.") + token, err = getMatchingTokenRPC(ctx, c, from, ctx.String("token")) if err != nil { return cli.NewExitError(err, 1) } @@ -470,10 +471,10 @@ func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account if err != nil { return cli.NewExitError(err, 1) } - fmt.Println(res.StringLE()) + fmt.Fprintln(ctx.App.Writer, res.StringLE()) return nil } - fmt.Println(tx.Hash().StringLE()) + fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) return nil } diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index fc03899cb..64c12d51c 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -83,7 +83,7 @@ func handleCandidate(ctx *cli.Context, method string) error { addrFlag := ctx.Generic("address").(*flags.Address) addr := addrFlag.Uint160() - acc, err := getDecryptedAccount(wall, addr) + acc, err := getDecryptedAccount(ctx, wall, addr) if err != nil { return cli.NewExitError(err, 1) } @@ -111,7 +111,7 @@ func handleCandidate(ctx *cli.Context, method string) error { if err != nil { return cli.NewExitError(err, 1) } - fmt.Println(res.StringLE()) + fmt.Fprintln(ctx.App.Writer, res.StringLE()) return nil } @@ -123,7 +123,7 @@ func handleVote(ctx *cli.Context) error { addrFlag := ctx.Generic("address").(*flags.Address) addr := addrFlag.Uint160() - acc, err := getDecryptedAccount(wall, addr) + acc, err := getDecryptedAccount(ctx, wall, addr) if err != nil { return cli.NewExitError(err, 1) } @@ -168,17 +168,17 @@ func handleVote(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - fmt.Println(res.StringLE()) + fmt.Fprintln(ctx.App.Writer, res.StringLE()) return nil } -func getDecryptedAccount(wall *wallet.Wallet, addr util.Uint160) (*wallet.Account, error) { +func getDecryptedAccount(ctx *cli.Context, wall *wallet.Wallet, addr util.Uint160) (*wallet.Account, error) { acc := wall.GetAccount(addr) if acc == nil { return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr)) } - if pass, err := readPassword("Password > "); err != nil { + if pass, err := readPassword(ctx.App.Writer, "Password > "); err != nil { return nil, err } else if err := acc.Decrypt(pass); err != nil { return nil, err diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 54c5e4f69..30b742512 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "io" "os" "strings" "syscall" @@ -222,7 +223,7 @@ func claimGas(ctx *cli.Context) error { return cli.NewExitError("address was not provided", 1) } scriptHash := addrFlag.Uint160() - acc, err := getDecryptedAccount(wall, scriptHash) + acc, err := getDecryptedAccount(ctx, wall, scriptHash) if err != nil { return cli.NewExitError(err, 1) } @@ -245,7 +246,7 @@ func claimGas(ctx *cli.Context) error { return cli.NewExitError(err, 1) } - fmt.Println(hash.StringLE()) + fmt.Fprintln(ctx.App.Writer, hash.StringLE()) return nil } @@ -265,7 +266,7 @@ func convertWallet(ctx *cli.Context) error { for _, acc := range wall.Accounts { address.Prefix = address.NEO2Prefix - pass, err := readPassword(fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label)) + pass, err := readPassword(ctx.App.Writer, fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label)) if err != nil { return cli.NewExitError(err, -1) } else if err := acc.Decrypt(pass); err != nil { @@ -347,7 +348,7 @@ loop: for _, wif := range wifs { if decrypt { - pass, err := readPassword("Enter password > ") + pass, err := readPassword(ctx.App.Writer, "Enter password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -360,7 +361,7 @@ loop: wif = pk.WIF() } - fmt.Println(wif) + fmt.Fprintln(ctx.App.Writer, wif) } return nil @@ -389,7 +390,7 @@ func importMultisig(ctx *cli.Context) error { } } - acc, err := newAccountFromWIF(ctx.String("wif")) + acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif")) if err != nil { return cli.NewExitError(err, 1) } @@ -419,7 +420,7 @@ func importDeployed(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("invalid contract hash: %w", err), 1) } - acc, err := newAccountFromWIF(ctx.String("wif")) + acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif")) if err != nil { return cli.NewExitError(err, 1) } @@ -463,7 +464,7 @@ func importWallet(ctx *cli.Context) error { } defer wall.Close() - acc, err := newAccountFromWIF(ctx.String("wif")) + acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif")) if err != nil { return cli.NewExitError(err, 1) } @@ -502,8 +503,8 @@ func removeAccount(ctx *cli.Context) error { } if !ctx.Bool("force") { - fmt.Printf("Account %s will be removed. This action is irreversible.\n", addrArg) - if ok := askForConsent(); !ok { + fmt.Fprintf(ctx.App.Writer, "Account %s will be removed. This action is irreversible.\n", addrArg) + if ok := askForConsent(ctx.App.Writer); !ok { return nil } } @@ -516,8 +517,8 @@ func removeAccount(ctx *cli.Context) error { return nil } -func askForConsent() bool { - fmt.Print("Are you sure? [y/N]: ") +func askForConsent(w io.Writer) bool { + fmt.Fprintln(w, "Are you sure? [y/N]: ") reader := bufio.NewReader(os.Stdin) response, err := reader.ReadString('\n') if err == nil { @@ -526,7 +527,7 @@ func askForConsent() bool { return true } } - fmt.Println("Cancelled.") + fmt.Fprintln(w, "Cancelled.") return false } @@ -536,7 +537,7 @@ func dumpWallet(ctx *cli.Context) error { return cli.NewExitError(err, 1) } if ctx.Bool("decrypt") { - pass, err := readPassword("Enter wallet password > ") + pass, err := readPassword(ctx.App.Writer, "Enter wallet password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -548,7 +549,7 @@ func dumpWallet(ctx *cli.Context) error { } } } - fmtPrintWallet(wall) + fmtPrintWallet(ctx.App.Writer, wall) return nil } @@ -571,20 +572,20 @@ func createWallet(ctx *cli.Context) error { } } - fmtPrintWallet(wall) - fmt.Printf("wallet successfully created, file location is %s\n", wall.Path()) + fmtPrintWallet(ctx.App.Writer, wall) + fmt.Fprintf(ctx.App.Writer, "wallet successfully created, file location is %s\n", wall.Path()) return nil } -func readAccountInfo() (string, string, error) { +func readAccountInfo(w io.Writer) (string, string, error) { buf := bufio.NewReader(os.Stdin) - fmt.Print("Enter the name of the account > ") + fmt.Fprint(w, "Enter the name of the account > ") rawName, _ := buf.ReadBytes('\n') - phrase, err := readPassword("Enter passphrase > ") + phrase, err := readPassword(w, "Enter passphrase > ") if err != nil { return "", "", err } - phraseCheck, err := readPassword("Confirm passphrase > ") + phraseCheck, err := readPassword(w, "Confirm passphrase > ") if err != nil { return "", "", err } @@ -598,7 +599,7 @@ func readAccountInfo() (string, string, error) { } func createAccount(ctx *cli.Context, wall *wallet.Wallet) error { - name, phrase, err := readAccountInfo() + name, phrase, err := readAccountInfo(ctx.App.Writer) if err != nil { return err } @@ -612,11 +613,11 @@ func openWallet(path string) (*wallet.Wallet, error) { return wallet.NewWalletFromFile(path) } -func newAccountFromWIF(wif string) (*wallet.Account, error) { +func newAccountFromWIF(w io.Writer, wif string) (*wallet.Account, error) { // note: NEP2 strings always have length of 58 even though // base58 strings can have different lengths even if slice lengths are equal if len(wif) == 58 { - pass, err := readPassword("Enter password > ") + pass, err := readPassword(w, "Enter password > ") if err != nil { return nil, err } @@ -629,8 +630,8 @@ func newAccountFromWIF(wif string) (*wallet.Account, error) { return nil, err } - fmt.Println("Provided WIF was unencrypted. Wallet can contain only encrypted keys.") - name, pass, err := readAccountInfo() + fmt.Fprintln(w, "Provided WIF was unencrypted. Wallet can contain only encrypted keys.") + name, pass, err := readAccountInfo(w) if err != nil { return nil, err } @@ -654,19 +655,19 @@ func addAccountAndSave(w *wallet.Wallet, acc *wallet.Account) error { return w.Save() } -func readPassword(prompt string) (string, error) { - fmt.Print(prompt) +func readPassword(w io.Writer, prompt string) (string, error) { + fmt.Fprint(w, prompt) rawPass, err := terminal.ReadPassword(syscall.Stdin) - fmt.Println() + fmt.Fprintln(w) if err != nil { return "", err } return strings.TrimRight(string(rawPass), "\n"), nil } -func fmtPrintWallet(wall *wallet.Wallet) { +func fmtPrintWallet(w io.Writer, wall *wallet.Wallet) { b, _ := wall.JSON() - fmt.Println("") - fmt.Println(string(b)) - fmt.Println("") + fmt.Fprintln(w, "") + fmt.Fprintln(w, string(b)) + fmt.Fprintln(w, "") } From 758a88a8134d5bcf8f10d4acb23dc8b41090f90a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 31 Aug 2020 15:35:55 +0300 Subject: [PATCH 02/20] rpc: exit from Start after Listener is running If port is dynamically allocated, `(*Server).Addr` will contain 0 port. This commit executes listener before exiting from `Start()` and sets Addr to the actual address. --- cli/server/server.go | 2 +- pkg/rpc/server/server.go | 22 ++++++++++++++++++---- pkg/rpc/server/server_helper_test.go | 2 +- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/cli/server/server.go b/cli/server/server.go index ed5840d72..b4a874963 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -355,7 +355,7 @@ func startServer(ctx *cli.Context) error { errChan := make(chan error) go serv.Start(errChan) - go rpcServer.Start(errChan) + rpcServer.Start(errChan) fmt.Fprintln(ctx.App.Writer, logo()) fmt.Fprintln(ctx.App.Writer, serv.UserAgent) diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 3617fcebd..6668e7510 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -167,18 +167,32 @@ func (s *Server) Start(errChan chan error) { s.https.Handler = http.HandlerFunc(s.handleHTTPRequest) s.log.Info("starting rpc-server (https)", zap.String("endpoint", s.https.Addr)) go func() { - err := s.https.ListenAndServeTLS(cfg.CertFile, cfg.KeyFile) + ln, err := net.Listen("tcp", s.https.Addr) + if err != nil { + errChan <- err + return + } + s.https.Addr = ln.Addr().String() + err = s.https.ServeTLS(ln, cfg.CertFile, cfg.KeyFile) if err != http.ErrServerClosed { s.log.Error("failed to start TLS RPC server", zap.Error(err)) errChan <- err } }() } - err := s.ListenAndServe() - if err != http.ErrServerClosed { - s.log.Error("failed to start RPC server", zap.Error(err)) + ln, err := net.Listen("tcp", s.Addr) + if err != nil { errChan <- err + return } + s.Addr = ln.Addr().String() // set Addr to the actual address + go func() { + err = s.Serve(ln) + if err != http.ErrServerClosed { + s.log.Error("failed to start RPC server", zap.Error(err)) + errChan <- err + } + }() } // Shutdown overrides the http.Server Shutdown diff --git a/pkg/rpc/server/server_helper_test.go b/pkg/rpc/server/server_helper_test.go index d10b473ee..1ea4ab6c4 100644 --- a/pkg/rpc/server/server_helper_test.go +++ b/pkg/rpc/server/server_helper_test.go @@ -68,7 +68,7 @@ func initClearServerWithInMemoryChain(t *testing.T) (*core.Blockchain, *Server, require.NoError(t, err) rpcServer := New(chain, cfg.ApplicationConfiguration.RPC, server, logger) errCh := make(chan error, 2) - go rpcServer.Start(errCh) + rpcServer.Start(errCh) handler := http.HandlerFunc(rpcServer.handleHTTPRequest) srv := httptest.NewServer(handler) From 0dda24771953696acda224e5d3481fcc2bf6c1b2 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 31 Aug 2020 12:22:09 +0300 Subject: [PATCH 03/20] cli: move input handling to a separate package --- cli/input/input.go | 44 +++++++++++++++++++++++++++++ cli/smartcontract/smart_contract.go | 7 ++--- cli/testdata/testwallet.json | 1 + cli/wallet/validator.go | 4 ++- cli/wallet/wallet.go | 35 ++++++----------------- 5 files changed, 60 insertions(+), 31 deletions(-) create mode 100644 cli/input/input.go create mode 100644 cli/testdata/testwallet.json diff --git a/cli/input/input.go b/cli/input/input.go new file mode 100644 index 000000000..1a15f541e --- /dev/null +++ b/cli/input/input.go @@ -0,0 +1,44 @@ +package input + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + "syscall" + + "golang.org/x/crypto/ssh/terminal" +) + +// Terminal is a terminal used for input. If `nil`, stdin is used. +var Terminal *terminal.Terminal + +// ReadLine reads line from the input without trailing '\n' +func ReadLine(w io.Writer, prompt string) (string, error) { + if Terminal != nil { + _, err := Terminal.Write([]byte(prompt)) + if err != nil { + return "", err + } + raw, err := Terminal.ReadLine() + return strings.TrimRight(raw, "\n"), err + } + fmt.Fprint(w, prompt) + buf := bufio.NewReader(os.Stdin) + return buf.ReadString('\n') +} + +// ReadPassword reads user password with prompt. +func ReadPassword(w io.Writer, prompt string) (string, error) { + if Terminal != nil { + return Terminal.ReadPassword(prompt) + } + fmt.Fprint(w, prompt) + rawPass, err := terminal.ReadPassword(syscall.Stdin) + if err != nil { + return "", err + } + fmt.Fprintln(w) + return strings.TrimRight(string(rawPass), "\n"), nil +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 0410e87d7..5284630f5 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -9,9 +9,9 @@ import ( "os" "path/filepath" "strings" - "syscall" "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -25,7 +25,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" - "golang.org/x/crypto/ssh/terminal" "gopkg.in/yaml.v2" ) @@ -680,8 +679,8 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) } - fmt.Fprintf(ctx.App.Writer, "Enter account %s password > ", address.Uint160ToString(addr)) - rawPass, err := terminal.ReadPassword(syscall.Stdin) + rawPass, err := input.ReadPassword(ctx.App.Writer, + fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) fmt.Fprintln(ctx.App.Writer) if err != nil { return nil, cli.NewExitError(err, 1) diff --git a/cli/testdata/testwallet.json b/cli/testdata/testwallet.json new file mode 100644 index 000000000..97b2e913a --- /dev/null +++ b/cli/testdata/testwallet.json @@ -0,0 +1 @@ +{"version":"3.0","accounts":[{"address":"NNuJqXDnRqvwgzhSzhH4jnVFWB1DyZ34EM","key":"6PYT6enT6eh4gu4ew3Mx58pFFDQNhuR1qQPuU594Eo5u4sA2ZvE4MqJV12","label":"kek","contract":{"script":"DCECl3UyEIq6T5RRIXS6z4tNdZPTzQ7NvXyx7FwK05d9UyYLQZVEDXg=","parameters":[{"name":"parameter0","type":"Signature"}],"deployed":false},"lock":false,"isdefault":false}],"scrypt":{"n":16384,"r":8,"p":8},"extra":{"Tokens":null}} diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 64c12d51c..0d9d7ad97 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -178,7 +179,8 @@ func getDecryptedAccount(ctx *cli.Context, wall *wallet.Wallet, addr util.Uint16 return nil, fmt.Errorf("can't find account for the address: %s", address.Uint160ToString(addr)) } - if pass, err := readPassword(ctx.App.Writer, "Password > "); err != nil { + if pass, err := input.ReadPassword(ctx.App.Writer, "Password > "); err != nil { + fmt.Println("ERROR", pass, err) return nil, err } else if err := acc.Decrypt(pass); err != nil { return nil, err diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 30b742512..c74e7a17b 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -1,16 +1,14 @@ package wallet import ( - "bufio" "encoding/hex" "errors" "fmt" "io" - "os" "strings" - "syscall" "github.com/nspcc-dev/neo-go/cli/flags" + "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -18,7 +16,6 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" - "golang.org/x/crypto/ssh/terminal" ) var ( @@ -266,7 +263,7 @@ func convertWallet(ctx *cli.Context) error { for _, acc := range wall.Accounts { address.Prefix = address.NEO2Prefix - pass, err := readPassword(ctx.App.Writer, fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label)) + pass, err := input.ReadPassword(ctx.App.Writer, fmt.Sprintf("Enter passphrase for account %s (label '%s') > ", acc.Address, acc.Label)) if err != nil { return cli.NewExitError(err, -1) } else if err := acc.Decrypt(pass); err != nil { @@ -348,7 +345,7 @@ loop: for _, wif := range wifs { if decrypt { - pass, err := readPassword(ctx.App.Writer, "Enter password > ") + pass, err := input.ReadPassword(ctx.App.Writer, "Enter password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -518,9 +515,7 @@ func removeAccount(ctx *cli.Context) error { } func askForConsent(w io.Writer) bool { - fmt.Fprintln(w, "Are you sure? [y/N]: ") - reader := bufio.NewReader(os.Stdin) - response, err := reader.ReadString('\n') + response, err := input.ReadLine(w, "Are you sure? [y/N]: ") if err == nil { response = strings.ToLower(strings.TrimSpace(response)) if response == "y" || response == "yes" { @@ -537,7 +532,7 @@ func dumpWallet(ctx *cli.Context) error { return cli.NewExitError(err, 1) } if ctx.Bool("decrypt") { - pass, err := readPassword(ctx.App.Writer, "Enter wallet password > ") + pass, err := input.ReadPassword(ctx.App.Writer, "Enter wallet password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -578,14 +573,12 @@ func createWallet(ctx *cli.Context) error { } func readAccountInfo(w io.Writer) (string, string, error) { - buf := bufio.NewReader(os.Stdin) - fmt.Fprint(w, "Enter the name of the account > ") - rawName, _ := buf.ReadBytes('\n') - phrase, err := readPassword(w, "Enter passphrase > ") + rawName, _ := input.ReadLine(w, "Enter the name of the account > ") + phrase, err := input.ReadPassword(w, "Enter passphrase > ") if err != nil { return "", "", err } - phraseCheck, err := readPassword(w, "Confirm passphrase > ") + phraseCheck, err := input.ReadPassword(w, "Confirm passphrase > ") if err != nil { return "", "", err } @@ -617,7 +610,7 @@ func newAccountFromWIF(w io.Writer, wif string) (*wallet.Account, error) { // note: NEP2 strings always have length of 58 even though // base58 strings can have different lengths even if slice lengths are equal if len(wif) == 58 { - pass, err := readPassword(w, "Enter password > ") + pass, err := input.ReadPassword(w, "Enter password > ") if err != nil { return nil, err } @@ -655,16 +648,6 @@ func addAccountAndSave(w *wallet.Wallet, acc *wallet.Account) error { return w.Save() } -func readPassword(w io.Writer, prompt string) (string, error) { - fmt.Fprint(w, prompt) - rawPass, err := terminal.ReadPassword(syscall.Stdin) - fmt.Fprintln(w) - if err != nil { - return "", err - } - return strings.TrimRight(string(rawPass), "\n"), nil -} - func fmtPrintWallet(w io.Writer, wall *wallet.Wallet) { b, _ := wall.JSON() fmt.Fprintln(w, "") From 282b55494b5891023919711919e2bd02b883254d Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 19:58:51 +0300 Subject: [PATCH 04/20] consensus: allow to shutdown service --- pkg/consensus/consensus.go | 12 ++++++++++++ pkg/network/server.go | 3 +++ 2 files changed, 15 insertions(+) diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index 9b29cbde7..e0b871aa7 100644 --- a/pkg/consensus/consensus.go +++ b/pkg/consensus/consensus.go @@ -42,6 +42,8 @@ type Service interface { // Start initializes dBFT and starts event loop for consensus service. // It must be called only when sufficient amount of peers are connected. Start() + // Shutdown stops dBFT event loop. + Shutdown() // OnPayload is a callback to notify Service about new received payload. OnPayload(p *Payload) @@ -73,6 +75,7 @@ type service struct { // started is a flag set with Start method that runs an event handling // goroutine. started *atomic.Bool + quit chan struct{} } // Config is a configuration for consensus services. @@ -115,6 +118,7 @@ func NewService(cfg Config) (Service, error) { blockEvents: make(chan *coreb.Block, 1), network: cfg.Chain.GetConfig().Magic, started: atomic.NewBool(false), + quit: make(chan struct{}), } if cfg.Wallet == nil { @@ -190,9 +194,17 @@ func (s *service) Start() { } } +// Shutdown implements Service interface. +func (s *service) Shutdown() { + close(s.quit) +} + func (s *service) eventLoop() { for { select { + case <-s.quit: + s.dbft.Timer.Stop() + return case <-s.dbft.Timer.C(): hv := s.dbft.Timer.HV() s.log.Debug("timer fired", diff --git a/pkg/network/server.go b/pkg/network/server.go index 661764329..467c128f3 100644 --- a/pkg/network/server.go +++ b/pkg/network/server.go @@ -187,6 +187,9 @@ func (s *Server) Shutdown() { s.log.Info("shutting down server", zap.Int("peers", s.PeerCount())) s.transport.Close() s.discovery.Close() + if s.consensusStarted.Load() { + s.consensus.Shutdown() + } for p := range s.Peers() { p.Disconnect(errServerShutdown) } From 468f7ee65034cb0a4a9cddfb98f847c9d3abab28 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Sat, 29 Aug 2020 14:41:33 +0300 Subject: [PATCH 05/20] cli: add tests for NEP5 balance querying --- cli/executor_test.go | 148 ++++++++++++++++++++++++ cli/main.go | 13 ++- cli/main_test.go | 13 +++ cli/nep5_test.go | 46 ++++++++ cli/testdata/wallet1_solo.json | 72 ++++++++++++ config/protocol.unit_testnet.single.yml | 52 +++++++++ 6 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 cli/executor_test.go create mode 100644 cli/main_test.go create mode 100644 cli/nep5_test.go create mode 100644 cli/testdata/wallet1_solo.json create mode 100644 config/protocol.unit_testnet.single.yml diff --git a/cli/executor_test.go b/cli/executor_test.go new file mode 100644 index 000000000..ce35b0143 --- /dev/null +++ b/cli/executor_test.go @@ -0,0 +1,148 @@ +package main + +import ( + "bytes" + "errors" + "io" + "testing" + + "github.com/nspcc-dev/neo-go/cli/input" + "github.com/nspcc-dev/neo-go/pkg/config" + "github.com/nspcc-dev/neo-go/pkg/core" + "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/network" + "github.com/nspcc-dev/neo-go/pkg/rpc/server" + "github.com/stretchr/testify/require" + "github.com/urfave/cli" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" +) + +const ( + validatorAddr = "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK" + + validatorWallet = "testdata/wallet1_solo.json" +) + +var validatorHash, _ = address.StringToUint160(validatorAddr) + +// executor represents context for a test instance. +// It can be safely used in multiple tests, but not in parallel. +type executor struct { + // CLI is a cli application to test. + CLI *cli.App + // Chain is a blockchain instance (can be empty). + Chain *core.Blockchain + // RPC is an RPC server to query (can be empty). + RPC *server.Server + // NetSrv is a network server (can be empty). + NetSrv *network.Server + // Out contains command output. + Out *bytes.Buffer + // Err contains command errors. + Err *bytes.Buffer +} + +func newTestChain(t *testing.T) (*core.Blockchain, *server.Server, *network.Server) { + configPath := "../config/protocol.unit_testnet.single.yml" + cfg, err := config.LoadFile(configPath) + require.NoError(t, err, "could not load config") + + memoryStore := storage.NewMemoryStore() + logger := zaptest.NewLogger(t) + chain, err := core.NewBlockchain(memoryStore, cfg.ProtocolConfiguration, logger) + require.NoError(t, err, "could not create chain") + + go chain.Run() + + serverConfig := network.NewServerConfig(cfg) + netSrv, err := network.NewServer(serverConfig, chain, zap.NewNop()) + require.NoError(t, err) + go netSrv.Start(make(chan error, 1)) + rpcServer := server.New(chain, cfg.ApplicationConfiguration.RPC, netSrv, logger) + errCh := make(chan error, 2) + rpcServer.Start(errCh) + + return chain, &rpcServer, netSrv +} + +func newExecutor(t *testing.T, needChain bool) *executor { + e := &executor{ + CLI: newApp(), + Out: bytes.NewBuffer(nil), + Err: bytes.NewBuffer(nil), + } + e.CLI.Writer = e.Out + e.CLI.ErrWriter = e.Err + if needChain { + e.Chain, e.RPC, e.NetSrv = newTestChain(t) + } + return e +} + +func (e *executor) Close(t *testing.T) { + input.Terminal = nil + if e.RPC != nil { + require.NoError(t, e.RPC.Shutdown()) + } + if e.NetSrv != nil { + e.NetSrv.Shutdown() + } + if e.Chain != nil { + e.Chain.Close() + } +} + +func (e *executor) checkNextLine(t *testing.T, expected string) { + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + e.checkLine(t, line, expected) +} + +func (e *executor) checkLine(t *testing.T, line, expected string) { + require.Regexp(t, expected, line) +} + +func (e *executor) checkEOF(t *testing.T) { + _, err := e.Out.ReadString('\n') + require.True(t, errors.Is(err, io.EOF)) +} + +func setExitFunc() <-chan int { + ch := make(chan int, 1) + cli.OsExiter = func(code int) { + ch <- code + } + return ch +} + +func checkExit(t *testing.T, ch <-chan int, code int) { + select { + case c := <-ch: + require.Equal(t, code, c) + default: + if code != 0 { + require.Fail(t, "no exit was called") + } + } +} + +// RunWithError runs command and checks that is exits with error. +func (e *executor) RunWithError(t *testing.T, args ...string) { + ch := setExitFunc() + require.Error(t, e.run(args...)) + checkExit(t, ch, 1) +} + +// Run runs command and checks that there were no errors. +func (e *executor) Run(t *testing.T, args ...string) { + ch := setExitFunc() + require.NoError(t, e.run(args...)) + checkExit(t, ch, 0) +} +func (e *executor) run(args ...string) error { + e.Out.Reset() + e.Err.Reset() + return e.CLI.Run(args) +} diff --git a/cli/main.go b/cli/main.go index b3494b675..5d762a545 100644 --- a/cli/main.go +++ b/cli/main.go @@ -13,6 +13,14 @@ import ( ) func main() { + ctl := newApp() + + if err := ctl.Run(os.Args); err != nil { + panic(err) + } +} + +func newApp() *cli.App { ctl := cli.NewApp() ctl.Name = "neo-go" ctl.Version = config.Version @@ -23,8 +31,5 @@ func main() { ctl.Commands = append(ctl.Commands, wallet.NewCommands()...) ctl.Commands = append(ctl.Commands, vm.NewCommands()...) ctl.Commands = append(ctl.Commands, util.NewCommands()...) - - if err := ctl.Run(os.Args); err != nil { - panic(err) - } + return ctl } diff --git a/cli/main_test.go b/cli/main_test.go new file mode 100644 index 000000000..c5ac7a622 --- /dev/null +++ b/cli/main_test.go @@ -0,0 +1,13 @@ +package main + +import ( + "testing" +) + +func TestCLIVersion(t *testing.T) { + e := newExecutor(t, false) + defer e.Close(t) + e.Run(t, "neo-go", "--version") + e.checkNextLine(t, "^neo-go version") + e.checkEOF(t) +} diff --git a/cli/nep5_test.go b/cli/nep5_test.go new file mode 100644 index 000000000..81fcdd8de --- /dev/null +++ b/cli/nep5_test.go @@ -0,0 +1,46 @@ +package main + +import ( + "strconv" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/util" +) + +func TestNEP5Balance(t *testing.T) { + e := newExecutor(t, true) + defer e.Close(t) + cmd := []string{ + "neo-go", "wallet", "nep5", "balance", + "--rpc-endpoint", "http://" + e.RPC.Addr, + "--wallet", validatorWallet, + "--addr", validatorAddr, + } + t.Run("NEO", func(t *testing.T) { + b, index := e.Chain.GetGoverningTokenBalance(validatorHash) + checkResult := func(t *testing.T) { + e.checkNextLine(t, "^\\s*TokenHash:\\s*"+e.Chain.GoverningTokenHash().StringLE()) + e.checkNextLine(t, "^\\s*Amount\\s*:\\s*"+b.String()) + e.checkNextLine(t, "^\\s*Updated\\s*:\\s*"+strconv.FormatUint(uint64(index), 10)) + e.checkEOF(t) + } + t.Run("Alias", func(t *testing.T) { + e.Run(t, append(cmd, "--token", "neo")...) + checkResult(t) + }) + t.Run("Hash", func(t *testing.T) { + e.Run(t, append(cmd, "--token", e.Chain.GoverningTokenHash().StringLE())...) + checkResult(t) + }) + }) + t.Run("GAS", func(t *testing.T) { + e.Run(t, append(cmd, "--token", "gas")...) + e.checkNextLine(t, "^\\s*TokenHash:\\s*"+e.Chain.UtilityTokenHash().StringLE()) + b := e.Chain.GetUtilityTokenBalance(validatorHash) + e.checkNextLine(t, "^\\s*Amount\\s*:\\s*"+util.Fixed8(b.Int64()).String()) + }) + t.Run("Invalid", func(t *testing.T) { + e.RunWithError(t, append(cmd, "--token", "kek")...) + }) + return +} diff --git a/cli/testdata/wallet1_solo.json b/cli/testdata/wallet1_solo.json new file mode 100644 index 000000000..1612925d6 --- /dev/null +++ b/cli/testdata/wallet1_solo.json @@ -0,0 +1,72 @@ +{ + "version": "3.0", + "accounts": [ + { + "address": "NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc", + "key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux", + "label": "", + "contract": { + "script": "DCECs2Ir9AF73+MXxYrtX0x1PyBrfbiWBG+n13S7xL9/jcILQZVEDXg=", + "parameters": [ + { + "name": "parameter0", + "type": "Signature" + } + ], + "deployed": false + }, + "lock": false, + "isdefault": false + }, + { + "address": "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY", + "key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux", + "label": "", + "contract": { + "script": "EwwhAhA6f33QFlWFl/eWDSfFFqQ5T9loueZRVetLAT5AQEBuDCECp7xV/oaE4BGXaNEEujB5W9zIZhnoZK3SYVZyPtGFzWIMIQKzYiv0AXvf4xfFiu1fTHU/IGt9uJYEb6fXdLvEv3+NwgwhA9kMB99j5pDOd5EuEKtRrMlEtmhgI3tgjE+PgwnnHuaZFAtBE43vrw==", + "parameters": [ + { + "name": "parameter0", + "type": "Signature" + }, + { + "name": "parameter1", + "type": "Signature" + }, + { + "name": "parameter2", + "type": "Signature" + } + ], + "deployed": false + }, + "lock": false, + "isdefault": false + }, + { + "address": "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK", + "key": "6PYN7LvaWqBNw7Xb7a52LSbPnP91kyuzYi3HncGvQwQoYAY2W8DncTgpux", + "label": "", + "contract": { + "script": "EQwhArNiK/QBe9/jF8WK7V9MdT8ga324lgRvp9d0u8S/f43CEQtBE43vrw==", + "parameters": [ + { + "name": "parameter0", + "type": "Signature" + } + ], + "deployed": false + }, + "lock": false, + "isdefault": false + } + ], + "scrypt": { + "n": 16384, + "r": 8, + "p": 8 + }, + "extra": { + "Tokens": null + } +} diff --git a/config/protocol.unit_testnet.single.yml b/config/protocol.unit_testnet.single.yml new file mode 100644 index 000000000..0d9646de6 --- /dev/null +++ b/config/protocol.unit_testnet.single.yml @@ -0,0 +1,52 @@ +ProtocolConfiguration: + Magic: 42 + SecondsPerBlock: 1 + MemPoolSize: 100 + StandbyCommittee: + - 02b3622bf4017bdfe317c58aed5f4c753f206b7db896046fa7d774bbc4bf7f8dc2 + ValidatorsCount: 1 + VerifyBlocks: true + VerifyTransactions: true + +ApplicationConfiguration: + # LogPath could be set up in case you need stdout logs to some proper file. + # LogPath: "./log/neogo.log" + DBConfiguration: + Type: "inmemory" #other options: 'inmemory','redis','boltdb', 'badgerdb'. + # DB type options. Uncomment those you need in case you want to switch DB type. + # LevelDBOptions: + # DataDirectoryPath: "./chains/unit_testnet" + # RedisDBOptions: + # Addr: "localhost:6379" + # Password: "" + # DB: 0 + # BoltDBOptions: + # FilePath: "./chains/unit_testnet.bolt" + # BadgerDBOptions: + # BadgerDir: "./chains/unit_testnet.badger" + # Uncomment in order to set up custom address for node. + # Address: 127.0.0.1 + NodePort: 0 + Relay: true + DialTimeout: 3 + ProtoTickInterval: 2 + PingInterval: 30 + PingTimeout: 90 + MinPeers: 0 + MaxPeers: 10 + AttemptConnPeers: 5 + UnlockWallet: + Path: "testdata/wallet1_solo.json" + Password: "one" + RPC: + Address: 127.0.0.1 + MaxGasInvoke: 10 + Enabled: true + EnableCORSWorkaround: false + Port: 0 # let the system choose port dynamically + Prometheus: + Enabled: false #since it's not useful for unit tests. + Port: 2112 + Pprof: + Enabled: false #since it's not useful for unit tests. + Port: 2113 From d30c7db49abe5f6851301b91898fcc09dc2c5b4a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 31 Aug 2020 12:42:42 +0300 Subject: [PATCH 06/20] cli: add tests for NEP5 transfer --- cli/executor_test.go | 26 +++++++++++++++++++++++ cli/nep5_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++ cli/options/options.go | 4 ++++ 3 files changed, 78 insertions(+) diff --git a/cli/executor_test.go b/cli/executor_test.go index ce35b0143..142cfce3d 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -1,22 +1,28 @@ package main import ( + "bufio" "bytes" "errors" "io" + "io/ioutil" "testing" + "time" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/storage" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpc/server" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/stretchr/testify/require" "github.com/urfave/cli" "go.uber.org/zap" "go.uber.org/zap/zaptest" + "golang.org/x/crypto/ssh/terminal" ) const ( @@ -42,6 +48,8 @@ type executor struct { Out *bytes.Buffer // Err contains command errors. Err *bytes.Buffer + // In contains command input. + In *bytes.Buffer } func newTestChain(t *testing.T) (*core.Blockchain, *server.Server, *network.Server) { @@ -72,9 +80,13 @@ func newExecutor(t *testing.T, needChain bool) *executor { CLI: newApp(), Out: bytes.NewBuffer(nil), Err: bytes.NewBuffer(nil), + In: bytes.NewBuffer(nil), } e.CLI.Writer = e.Out e.CLI.ErrWriter = e.Err + rw := bufio.NewReadWriter(bufio.NewReader(e.In), bufio.NewWriter(ioutil.Discard)) + require.Nil(t, input.Terminal) // check that tests clean up properly + input.Terminal = terminal.NewTerminal(rw, "") if needChain { e.Chain, e.RPC, e.NetSrv = newTestChain(t) } @@ -94,6 +106,20 @@ func (e *executor) Close(t *testing.T) { } } +// GetTransaction returns tx with hash h after it has persisted. +// If it is in mempool, we can just wait for the next block, otherwise +// it must be already in chain. 1 second is time per block in a unittest chain. +func (e *executor) GetTransaction(t *testing.T, h util.Uint256) *transaction.Transaction { + var tx *transaction.Transaction + require.Eventually(t, func() bool { + var height uint32 + var err error + tx, height, err = e.Chain.GetTransaction(h) + return err == nil && height != 0 + }, time.Second*2, time.Millisecond*100, "too long time waiting for block") + return tx +} + func (e *executor) checkNextLine(t *testing.T, expected string) { line, err := e.Out.ReadString('\n') require.NoError(t, err) diff --git a/cli/nep5_test.go b/cli/nep5_test.go index 81fcdd8de..f9a639cde 100644 --- a/cli/nep5_test.go +++ b/cli/nep5_test.go @@ -1,10 +1,16 @@ package main import ( + "math/big" "strconv" + "strings" "testing" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" ) func TestNEP5Balance(t *testing.T) { @@ -44,3 +50,45 @@ func TestNEP5Balance(t *testing.T) { }) return } + +func TestNEP5Transfer(t *testing.T) { + w, err := wallet.NewWalletFromFile("testdata/testwallet.json") + require.NoError(t, err) + defer w.Close() + + e := newExecutor(t, true) + defer e.Close(t) + args := []string{ + "neo-go", "wallet", "nep5", "transfer", + "--unittest", + "--rpc-endpoint", "http://" + e.RPC.Addr, + "--wallet", validatorWallet, + "--from", validatorAddr, + "--to", w.Accounts[0].Address, + "--token", "neo", + "--amount", "1", + } + + t.Run("InvalidPassword", func(t *testing.T) { + e.In.WriteString("onetwothree\r") + e.RunWithError(t, args...) + e.In.Reset() + }) + + e.In.WriteString("one\r") + e.Run(t, args...) + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + h, err := util.Uint256DecodeStringLE(strings.TrimSpace(line)) + require.NoError(t, err, "can't decode tx hash: %s", line) + + tx := e.GetTransaction(t, h) + aer, err := e.Chain.GetAppExecResult(tx.Hash()) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer.VMState) + + sh, err := address.StringToUint160(w.Accounts[0].Address) + require.NoError(t, err) + b, _ := e.Chain.GetGoverningTokenBalance(sh) + require.Equal(t, big.NewInt(1), b) +} diff --git a/cli/options/options.go b/cli/options/options.go index 19420db0f..19f4c7e4e 100644 --- a/cli/options/options.go +++ b/cli/options/options.go @@ -37,6 +37,7 @@ var RPC = []cli.Flag{ cli.BoolFlag{Name: "privnet, p"}, cli.BoolFlag{Name: "mainnet, m"}, cli.BoolFlag{Name: "testnet, t"}, + cli.BoolFlag{Name: "unittest", Hidden: true}, } var errNoEndpoint = errors.New("no RPC endpoint specified, use option '--" + RPCEndpointFlag + "' or '-r'") @@ -51,6 +52,9 @@ func GetNetwork(ctx *cli.Context) netmode.Magic { if ctx.Bool("mainnet") { net = netmode.MainNet } + if ctx.Bool("unittest") { + net = netmode.UnitTestNet + } return net } From 5d7f177811e2f2f14b37671888398697251a7262 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Mon, 31 Aug 2020 16:17:44 +0300 Subject: [PATCH 07/20] cli: add tests for wallet/account creation --- cli/wallet/wallet.go | 4 ++- cli/wallet_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 cli/wallet_test.go diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index c74e7a17b..e8f8e2389 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -474,7 +474,9 @@ func importWallet(ctx *cli.Context) error { acc.Contract.Script = ctr } - acc.Label = ctx.String("name") + if acc.Label == "" { + acc.Label = ctx.String("name") + } if err := addAccountAndSave(wall, acc); err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet_test.go b/cli/wallet_test.go new file mode 100644 index 000000000..2a57a2fdf --- /dev/null +++ b/cli/wallet_test.go @@ -0,0 +1,85 @@ +package main + +import ( + "os" + "path" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +func TestWalletInit(t *testing.T) { + tmpDir := os.TempDir() + e := newExecutor(t, false) + defer e.Close(t) + + walletPath := path.Join(tmpDir, "wallet.json") + e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath) + defer os.Remove(walletPath) + + t.Run("CreateAccount", func(t *testing.T) { + e.In.WriteString("testname\r") + e.In.WriteString("testpass\r") + e.In.WriteString("testpass\r") + e.Run(t, "neo-go", "wallet", "create", "--wallet", walletPath) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + defer w.Close() + require.Len(t, w.Accounts, 1) + require.Equal(t, w.Accounts[0].Label, "testname") + require.NoError(t, w.Accounts[0].Decrypt("testpass")) + }) + + t.Run("Import", func(t *testing.T) { + t.Run("WIF", func(t *testing.T) { + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + e.In.WriteString("test_account\r") + e.In.WriteString("qwerty\r") + e.In.WriteString("qwerty\r") + e.Run(t, "neo-go", "wallet", "import", "--wallet", walletPath, + "--wif", priv.WIF()) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + defer w.Close() + acc := w.GetAccount(priv.GetScriptHash()) + require.NotNil(t, acc) + require.Equal(t, "test_account", acc.Label) + require.NoError(t, acc.Decrypt("qwerty")) + + t.Run("AlreadyExists", func(t *testing.T) { + e.In.WriteString("test_account_2\r") + e.In.WriteString("qwerty2\r") + e.In.WriteString("qwerty2\r") + e.RunWithError(t, "neo-go", "wallet", "import", + "--wallet", walletPath, "--wif", priv.WIF()) + }) + }) + t.Run("EncryptedWIF", func(t *testing.T) { + acc, err := wallet.NewAccount() + require.NoError(t, err) + require.NoError(t, acc.Encrypt("somepass")) + + t.Run("InvalidPassword", func(t *testing.T) { + e.In.WriteString("password1\r") + e.RunWithError(t, "neo-go", "wallet", "import", "--wallet", walletPath, + "--wif", acc.EncryptedWIF) + }) + + e.In.WriteString("somepass\r") + e.Run(t, "neo-go", "wallet", "import", "--wallet", walletPath, + "--wif", acc.EncryptedWIF) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + defer w.Close() + actual := w.GetAccount(acc.PrivateKey().GetScriptHash()) + require.NotNil(t, actual) + require.NoError(t, actual.Decrypt("somepass")) + }) + }) +} From 0a5fe8458901969db24c781ab63a208642011ab5 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 14:55:10 +0300 Subject: [PATCH 08/20] cli: add tests for `wallet export` --- cli/executor_test.go | 7 ++++++- cli/wallet_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/cli/executor_test.go b/cli/executor_test.go index 142cfce3d..3fe1347c6 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpc/server" @@ -26,12 +27,16 @@ import ( ) const ( + validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" validatorAddr = "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK" validatorWallet = "testdata/wallet1_solo.json" ) -var validatorHash, _ = address.StringToUint160(validatorAddr) +var ( + validatorHash, _ = address.StringToUint160(validatorAddr) + validatorPriv, _ = keys.NewPrivateKeyFromWIF(validatorWIF) +) // executor represents context for a test instance. // It can be safely used in multiple tests, but not in parallel. diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 2a57a2fdf..2e80f7e53 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -3,6 +3,7 @@ package main import ( "os" "path" + "strings" "testing" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -83,3 +84,30 @@ func TestWalletInit(t *testing.T) { }) }) } + +func TestWalletExport(t *testing.T) { + e := newExecutor(t, false) + defer e.Close(t) + + t.Run("Encrypted", func(t *testing.T) { + e.Run(t, "neo-go", "wallet", "export", + "--wallet", validatorWallet, validatorAddr) + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + enc, err := keys.NEP2Encrypt(validatorPriv, "one") + require.NoError(t, err) + require.Equal(t, enc, strings.TrimSpace(line)) + }) + t.Run("Decrypted", func(t *testing.T) { + t.Run("NoAddress", func(t *testing.T) { + e.RunWithError(t, "neo-go", "wallet", "export", + "--wallet", validatorWallet, "--decrypt") + }) + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "export", + "--wallet", validatorWallet, "--decrypt", validatorAddr) + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + require.Equal(t, validatorWIF, strings.TrimSpace(line)) + }) +} From 4c00b26fb82a5c65704c6cf8171619ab02a8e0d6 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 15:05:07 +0300 Subject: [PATCH 09/20] cli: add tests for `wallet import-multisig` --- cli/wallet_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 2e80f7e53..073c7afbc 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -1,12 +1,15 @@ package main import ( + "encoding/hex" "os" "path" "strings" "testing" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -82,6 +85,50 @@ func TestWalletInit(t *testing.T) { require.NotNil(t, actual) require.NoError(t, actual.Decrypt("somepass")) }) + t.Run("Multisig", func(t *testing.T) { + privs := make([]*keys.PrivateKey, 4) + pubs := make(keys.PublicKeys, 4) + for i := range privs { + var err error + privs[i], err = keys.NewPrivateKey() + require.NoError(t, err) + pubs[i] = privs[i].PublicKey() + } + + cmd := []string{"neo-go", "wallet", "import-multisig", + "--wallet", walletPath, + "--wif", privs[0].WIF(), + "--min", "2"} + t.Run("InvalidPublicKeys", func(t *testing.T) { + e.In.WriteString("multiacc\r") + e.In.WriteString("multipass\r") + e.In.WriteString("multipass\r") + defer e.In.Reset() + + e.RunWithError(t, append(cmd, hex.EncodeToString(pubs[1].Bytes()), + hex.EncodeToString(pubs[1].Bytes()), + hex.EncodeToString(pubs[2].Bytes()), + hex.EncodeToString(pubs[3].Bytes()))...) + }) + e.In.WriteString("multiacc\r") + e.In.WriteString("multipass\r") + e.In.WriteString("multipass\r") + e.Run(t, append(cmd, hex.EncodeToString(pubs[0].Bytes()), + hex.EncodeToString(pubs[1].Bytes()), + hex.EncodeToString(pubs[2].Bytes()), + hex.EncodeToString(pubs[3].Bytes()))...) + + script, err := smartcontract.CreateMultiSigRedeemScript(2, pubs) + require.NoError(t, err) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + defer w.Close() + actual := w.GetAccount(hash.Hash160(script)) + require.NotNil(t, actual) + require.NoError(t, actual.Decrypt("multipass")) + require.Equal(t, script, actual.Contract.Script) + }) }) } From 59157724e30a81e224e71fe955611bc6b12e4d04 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 15:10:45 +0300 Subject: [PATCH 10/20] cli: add tests for `wallet remove` --- cli/wallet_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 073c7afbc..4662d93a5 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -31,10 +31,22 @@ func TestWalletInit(t *testing.T) { w, err := wallet.NewWalletFromFile(walletPath) require.NoError(t, err) - defer w.Close() require.Len(t, w.Accounts, 1) require.Equal(t, w.Accounts[0].Label, "testname") require.NoError(t, w.Accounts[0].Decrypt("testpass")) + w.Close() + + t.Run("RemoveAccount", func(t *testing.T) { + sh := w.Accounts[0].Contract.ScriptHash() + addr := w.Accounts[0].Address + e.In.WriteString("y\r") + e.Run(t, "neo-go", "wallet", "remove", + "--wallet", walletPath, addr) + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + require.Nil(t, w.GetAccount(sh)) + w.Close() + }) }) t.Run("Import", func(t *testing.T) { From 075f353062e281a910db755b6ab104d8c5c9005f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 15:20:38 +0300 Subject: [PATCH 11/20] cli: add tests for `nep5 multitransfer` Also fix a bug when token's hash was incorrectly processed. --- cli/nep5_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ cli/wallet/nep5.go | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/cli/nep5_test.go b/cli/nep5_test.go index f9a639cde..0a9013fa8 100644 --- a/cli/nep5_test.go +++ b/cli/nep5_test.go @@ -6,7 +6,9 @@ import ( "strings" "testing" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -92,3 +94,43 @@ func TestNEP5Transfer(t *testing.T) { b, _ := e.Chain.GetGoverningTokenBalance(sh) require.Equal(t, big.NewInt(1), b) } + +func TestNEP5MultiTransfer(t *testing.T) { + privs := make([]*keys.PrivateKey, 3) + for i := range privs { + var err error + privs[i], err = keys.NewPrivateKey() + require.NoError(t, err) + } + + e := newExecutor(t, true) + defer e.Close(t) + args := []string{ + "neo-go", "wallet", "nep5", "multitransfer", + "--unittest", "--rpc-endpoint", "http://" + e.RPC.Addr, + "--wallet", validatorWallet, + "--from", validatorAddr, + "neo:" + privs[0].Address() + ":42", + "GAS:" + privs[1].Address() + ":7", + client.NeoContractHash.StringLE() + ":" + privs[2].Address() + ":13", + } + + e.In.WriteString("one\r") + e.Run(t, args...) + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + h, err := util.Uint256DecodeStringLE(strings.TrimSpace(line)) + require.NoError(t, err, "can't decode tx hash: %s", line) + + tx := e.GetTransaction(t, h) + aer, err := e.Chain.GetAppExecResult(tx.Hash()) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer.VMState) + + b, _ := e.Chain.GetGoverningTokenBalance(privs[0].GetScriptHash()) + require.Equal(t, big.NewInt(42), b) + b = e.Chain.GetUtilityTokenBalance(privs[1].GetScriptHash()) + require.Equal(t, big.NewInt(int64(util.Fixed8FromInt64(7))), b) + b, _ = e.Chain.GetGoverningTokenBalance(privs[2].GetScriptHash()) + require.Equal(t, big.NewInt(13), b) +} diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index 2f7397f2f..0fa1d6350 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -373,7 +373,7 @@ func multiTransferNEP5(ctx *cli.Context) error { token, err = getMatchingToken(ctx, wall, ss[0]) if err != nil { fmt.Fprintln(ctx.App.ErrWriter, "Can't find matching token in the wallet. Querying RPC-node for balances.") - token, err = getMatchingTokenRPC(ctx, c, from, ctx.String("token")) + token, err = getMatchingTokenRPC(ctx, c, from, ss[0]) if err != nil { return cli.NewExitError(err, 1) } From 9817da7c4e3c9cc2db6548299daf2c69f25556fe Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 15:40:32 +0300 Subject: [PATCH 12/20] cli: add tests for `nep5 import/remove` Also unify `remove` and `import` arguments. --- cli/nep5_test.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++ cli/wallet/nep5.go | 8 ++----- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/cli/nep5_test.go b/cli/nep5_test.go index 0a9013fa8..4e25f46e2 100644 --- a/cli/nep5_test.go +++ b/cli/nep5_test.go @@ -1,7 +1,10 @@ package main import ( + "io" "math/big" + "os" + "path" "strconv" "strings" "testing" @@ -134,3 +137,59 @@ func TestNEP5MultiTransfer(t *testing.T) { b, _ = e.Chain.GetGoverningTokenBalance(privs[2].GetScriptHash()) require.Equal(t, big.NewInt(13), b) } + +func TestNEP5ImportToken(t *testing.T) { + e := newExecutor(t, true) + defer e.Close(t) + + tmpDir := os.TempDir() + walletPath := path.Join(tmpDir, "walletForImport.json") + defer os.Remove(walletPath) + + e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath) + e.Run(t, "neo-go", "wallet", "nep5", "import", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", walletPath, + "--token", client.GasContractHash.StringLE()) + e.Run(t, "neo-go", "wallet", "nep5", "import", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", walletPath, + "--token", client.NeoContractHash.StringLE()) + + t.Run("Info", func(t *testing.T) { + checkGASInfo := func(t *testing.T) { + e.checkNextLine(t, "^Name:\\s*GAS") + e.checkNextLine(t, "^Symbol:\\s*gas") + e.checkNextLine(t, "^Hash:\\s*"+client.GasContractHash.StringLE()) + e.checkNextLine(t, "^Decimals:\\s*8") + e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(client.GasContractHash)) + } + t.Run("WithToken", func(t *testing.T) { + e.Run(t, "neo-go", "wallet", "nep5", "info", + "--wallet", walletPath, "--token", client.GasContractHash.StringLE()) + checkGASInfo(t) + }) + t.Run("NoToken", func(t *testing.T) { + e.Run(t, "neo-go", "wallet", "nep5", "info", + "--wallet", walletPath) + checkGASInfo(t) + _, err := e.Out.ReadString('\n') + require.NoError(t, err) + e.checkNextLine(t, "^Name:\\s*NEO") + e.checkNextLine(t, "^Symbol:\\s*neo") + e.checkNextLine(t, "^Hash:\\s*"+client.NeoContractHash.StringLE()) + e.checkNextLine(t, "^Decimals:\\s*0") + e.checkNextLine(t, "^Address:\\s*"+address.Uint160ToString(client.NeoContractHash)) + }) + t.Run("Remove", func(t *testing.T) { + e.In.WriteString("y\r") + e.Run(t, "neo-go", "wallet", "nep5", "remove", + "--wallet", walletPath, "--token", client.NeoContractHash.StringLE()) + e.Run(t, "neo-go", "wallet", "nep5", "info", + "--wallet", walletPath) + checkGASInfo(t) + _, err := e.Out.ReadString('\n') + require.Equal(t, err, io.EOF) + }) + }) +} diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index 0fa1d6350..5ca1e293f 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -105,7 +105,7 @@ func newNEP5Commands() []cli.Command { { Name: "remove", Usage: "remove NEP5 token from the wallet", - UsageText: "remove --wallet ", + UsageText: "remove --wallet --token ", Action: removeNEP5Token, Flags: []cli.Flag{ walletPathFlag, @@ -314,11 +314,7 @@ func removeNEP5Token(ctx *cli.Context) error { } defer wall.Close() - name := ctx.Args().First() - if name == "" { - return cli.NewExitError("token must be specified", 1) - } - token, err := getMatchingToken(ctx, wall, name) + token, err := getMatchingToken(ctx, wall, ctx.String("token")) if err != nil { return cli.NewExitError(err, 1) } From 6e5a637da9c0a885845af07689dfe6a1c125729f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 16:24:01 +0300 Subject: [PATCH 13/20] cli: add tests for `multisig sign` This test is also a good example of how to create and sign multisig transaction from scratch. --- cli/executor_test.go | 28 ++++++++++++++ cli/multisig_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++ cli/nep5_test.go | 30 ++------------- cli/wallet/multisig.go | 7 +--- cli/wallet_test.go | 9 +---- 5 files changed, 118 insertions(+), 41 deletions(-) create mode 100644 cli/multisig_test.go diff --git a/cli/executor_test.go b/cli/executor_test.go index 3fe1347c6..4867633fe 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -6,6 +6,7 @@ import ( "errors" "io" "io/ioutil" + "strings" "testing" "time" @@ -19,6 +20,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/network" "github.com/nspcc-dev/neo-go/pkg/rpc/server" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/stretchr/testify/require" "github.com/urfave/cli" "go.uber.org/zap" @@ -177,3 +179,29 @@ func (e *executor) run(args ...string) error { e.Err.Reset() return e.CLI.Run(args) } + +func (e *executor) checkTxPersisted(t *testing.T) { + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + + line = strings.TrimSpace(line) + h, err := util.Uint256DecodeStringLE(line) + require.NoError(t, err, "can't decode tx hash: %s", line) + + tx := e.GetTransaction(t, h) + aer, err := e.Chain.GetAppExecResult(tx.Hash()) + require.NoError(t, err) + require.Equal(t, vm.HaltState, aer.VMState) +} + +func generateKeys(t *testing.T, n int) ([]*keys.PrivateKey, keys.PublicKeys) { + privs := make([]*keys.PrivateKey, n) + pubs := make(keys.PublicKeys, n) + for i := range privs { + var err error + privs[i], err = keys.NewPrivateKey() + require.NoError(t, err) + pubs[i] = privs[i].PublicKey() + } + return privs, pubs +} diff --git a/cli/multisig_test.go b/cli/multisig_test.go new file mode 100644 index 000000000..0d163764d --- /dev/null +++ b/cli/multisig_test.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/hex" + "math/big" + "os" + "path" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/stretchr/testify/require" +) + +// Test signing of multisig transactions. +// 1. Transfer funds to a created multisig address. +// 2. Transfer from multisig to another account. +func TestSignMultisigTx(t *testing.T) { + e := newExecutor(t, true) + defer e.Close(t) + + privs, pubs := generateKeys(t, 3) + script, err := smartcontract.CreateMultiSigRedeemScript(2, pubs) + require.NoError(t, err) + multisigHash := hash.Hash160(script) + multisigAddr := address.Uint160ToString(multisigHash) + + // Create 2 wallets participating in multisig. + tmpDir := os.TempDir() + wallet1Path := path.Join(tmpDir, "multiWallet1.json") + defer os.Remove(wallet1Path) + wallet2Path := path.Join(tmpDir, "multiWallet2.json") + defer os.Remove(wallet2Path) + + addAccount := func(w string, wif string) { + e.In.WriteString("acc\rpass\rpass\r") + e.Run(t, "neo-go", "wallet", "init", "--wallet", w) + e.Run(t, "neo-go", "wallet", "import-multisig", + "--wallet", w, + "--wif", wif, + "--min", "2", + hex.EncodeToString(pubs[0].Bytes()), + hex.EncodeToString(pubs[1].Bytes()), + hex.EncodeToString(pubs[2].Bytes())) + } + addAccount(wallet1Path, privs[0].WIF()) + addAccount(wallet2Path, privs[1].WIF()) + + // Transfer funds to the multisig. + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "nep5", "multitransfer", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, + "--from", validatorAddr, + "neo:"+multisigAddr+":4", + "gas:"+multisigAddr+":1") + e.checkTxPersisted(t) + + // Sign and transfer funds to another account. + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + txPath := path.Join(tmpDir, "multisigtx.json") + defer os.Remove(txPath) + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "nep5", "transfer", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", wallet1Path, "--from", multisigAddr, + "--to", priv.Address(), "--token", "neo", "--amount", "1", + "--out", txPath) + + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "multisig", "sign", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", wallet2Path, "--addr", multisigAddr, + "--in", txPath, "--out", txPath) + e.checkTxPersisted(t) + + b, _ := e.Chain.GetGoverningTokenBalance(priv.GetScriptHash()) + require.Equal(t, big.NewInt(1), b) + b, _ = e.Chain.GetGoverningTokenBalance(multisigHash) + require.Equal(t, big.NewInt(3), b) +} diff --git a/cli/nep5_test.go b/cli/nep5_test.go index 4e25f46e2..2f879ecc3 100644 --- a/cli/nep5_test.go +++ b/cli/nep5_test.go @@ -6,14 +6,11 @@ import ( "os" "path" "strconv" - "strings" "testing" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -82,15 +79,7 @@ func TestNEP5Transfer(t *testing.T) { e.In.WriteString("one\r") e.Run(t, args...) - line, err := e.Out.ReadString('\n') - require.NoError(t, err) - h, err := util.Uint256DecodeStringLE(strings.TrimSpace(line)) - require.NoError(t, err, "can't decode tx hash: %s", line) - - tx := e.GetTransaction(t, h) - aer, err := e.Chain.GetAppExecResult(tx.Hash()) - require.NoError(t, err) - require.Equal(t, vm.HaltState, aer.VMState) + e.checkTxPersisted(t) sh, err := address.StringToUint160(w.Accounts[0].Address) require.NoError(t, err) @@ -99,12 +88,7 @@ func TestNEP5Transfer(t *testing.T) { } func TestNEP5MultiTransfer(t *testing.T) { - privs := make([]*keys.PrivateKey, 3) - for i := range privs { - var err error - privs[i], err = keys.NewPrivateKey() - require.NoError(t, err) - } + privs, _ := generateKeys(t, 3) e := newExecutor(t, true) defer e.Close(t) @@ -120,15 +104,7 @@ func TestNEP5MultiTransfer(t *testing.T) { e.In.WriteString("one\r") e.Run(t, args...) - line, err := e.Out.ReadString('\n') - require.NoError(t, err) - h, err := util.Uint256DecodeStringLE(strings.TrimSpace(line)) - require.NoError(t, err, "can't decode tx hash: %s", line) - - tx := e.GetTransaction(t, h) - aer, err := e.Chain.GetAppExecResult(tx.Hash()) - require.NoError(t, err) - require.Equal(t, vm.HaltState, aer.VMState) + e.checkTxPersisted(t) b, _ := e.Chain.GetGoverningTokenBalance(privs[0].GetScriptHash()) require.Equal(t, big.NewInt(42), b) diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go index 87bd2dffc..58cf2012c 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -3,7 +3,6 @@ package wallet import ( "encoding/json" "fmt" - "io" "io/ioutil" "github.com/nspcc-dev/neo-go/cli/options" @@ -60,7 +59,7 @@ func signMultisig(ctx *cli.Context) error { if !ok { return cli.NewExitError("verifiable item is not a transaction", 1) } - printTxInfo(ctx.App.Writer, tx) + fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) priv := acc.PrivateKey() sign := priv.Sign(tx.GetSignedPart()) @@ -116,7 +115,3 @@ func writeParameterContext(c *context.ParameterContext, filename string) error { } return nil } - -func printTxInfo(w io.Writer, t *transaction.Transaction) { - fmt.Fprintf(w, "Hash: %s\n", t.Hash().StringLE()) -} diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 4662d93a5..de9649684 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -98,14 +98,7 @@ func TestWalletInit(t *testing.T) { require.NoError(t, actual.Decrypt("somepass")) }) t.Run("Multisig", func(t *testing.T) { - privs := make([]*keys.PrivateKey, 4) - pubs := make(keys.PublicKeys, 4) - for i := range privs { - var err error - privs[i], err = keys.NewPrivateKey() - require.NoError(t, err) - pubs[i] = privs[i].PublicKey() - } + privs, pubs := generateKeys(t, 4) cmd := []string{"neo-go", "wallet", "import-multisig", "--wallet", walletPath, From dff2ac1387a31b9d53b766defb678a311bd273c9 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Sep 2020 16:55:00 +0300 Subject: [PATCH 14/20] cli: add test for `candidate register/unregister/vote` Also fix a bug with tx signing. --- cli/candidate_test.go | 65 +++++++++++++++++++++++++++++++++++++++++ cli/wallet/validator.go | 13 +++++++-- 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 cli/candidate_test.go diff --git a/cli/candidate_test.go b/cli/candidate_test.go new file mode 100644 index 000000000..4600e2644 --- /dev/null +++ b/cli/candidate_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "encoding/hex" + "math/big" + "testing" + + "github.com/stretchr/testify/require" +) + +// Register standby validator and vote for it. +// We don't create a new account here, because chain will +// stop working after validator will change. +func TestRegisterCandidate(t *testing.T) { + e := newExecutor(t, true) + defer e.Close(t) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "nep5", "multitransfer", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, + "--from", validatorAddr, + "neo:"+validatorPriv.Address()+":10", + "gas:"+validatorPriv.Address()+":100") + e.checkTxPersisted(t) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "candidate", "register", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, + "--address", validatorPriv.Address()) + e.checkTxPersisted(t) + + vs, err := e.Chain.GetEnrollments() + require.NoError(t, err) + require.Equal(t, 1, len(vs)) + require.Equal(t, validatorPriv.PublicKey(), vs[0].Key) + require.Equal(t, big.NewInt(0), vs[0].Votes) + + t.Run("Vote", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "candidate", "vote", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, + "--address", validatorPriv.Address(), + "--candidate", hex.EncodeToString(validatorPriv.PublicKey().Bytes())) + e.checkTxPersisted(t) + + vs, err = e.Chain.GetEnrollments() + require.Equal(t, 1, len(vs)) + require.Equal(t, validatorPriv.PublicKey(), vs[0].Key) + b, _ := e.Chain.GetGoverningTokenBalance(validatorPriv.GetScriptHash()) + require.Equal(t, b, vs[0].Votes) + }) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "candidate", "unregister", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, + "--address", validatorPriv.Address()) + e.checkTxPersisted(t) + + vs, err = e.Chain.GetEnrollments() + require.Equal(t, 0, len(vs)) +} diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 0d9d7ad97..671f16d8f 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -6,6 +6,7 @@ import ( "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" @@ -81,6 +82,7 @@ func handleCandidate(ctx *cli.Context, method string) error { if err != nil { return cli.NewExitError(err, 1) } + defer wall.Close() addrFlag := ctx.Generic("address").(*flags.Address) addr := addrFlag.Uint160() @@ -101,7 +103,10 @@ func handleCandidate(ctx *cli.Context, method string) error { w := io.NewBufBinWriter() emit.AppCallWithOperationAndArgs(w.BinWriter, client.NeoContractHash, method, acc.PrivateKey().PublicKey().Bytes()) emit.Opcode(w.BinWriter, opcode.ASSERT) - tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas)) + tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas), transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }) if err != nil { return cli.NewExitError(err, 1) } else if err = acc.SignTx(tx); err != nil { @@ -121,6 +126,7 @@ func handleVote(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } + defer wall.Close() addrFlag := ctx.Generic("address").(*flags.Address) addr := addrFlag.Uint160() @@ -156,7 +162,10 @@ func handleVote(ctx *cli.Context) error { emit.AppCallWithOperationAndArgs(w.BinWriter, client.NeoContractHash, "vote", addr.BytesBE(), pubArg) emit.Opcode(w.BinWriter, opcode.ASSERT) - tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas)) + tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas), transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }) if err != nil { return cli.NewExitError(err, 1) } From 583ef546f99268351555cde3a90e158db2ab19c3 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Sep 2020 11:43:25 +0300 Subject: [PATCH 15/20] cli: add tests for `wallet claim` Fix a NEO contract hash used in tx. --- cli/executor_test.go | 11 ++++++----- cli/wallet/wallet.go | 8 ++------ cli/wallet_test.go | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/cli/executor_test.go b/cli/executor_test.go index 4867633fe..2a753d02b 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -116,15 +116,15 @@ func (e *executor) Close(t *testing.T) { // GetTransaction returns tx with hash h after it has persisted. // If it is in mempool, we can just wait for the next block, otherwise // it must be already in chain. 1 second is time per block in a unittest chain. -func (e *executor) GetTransaction(t *testing.T, h util.Uint256) *transaction.Transaction { +func (e *executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Transaction, uint32) { var tx *transaction.Transaction + var height uint32 require.Eventually(t, func() bool { - var height uint32 var err error tx, height, err = e.Chain.GetTransaction(h) return err == nil && height != 0 }, time.Second*2, time.Millisecond*100, "too long time waiting for block") - return tx + return tx, height } func (e *executor) checkNextLine(t *testing.T, expected string) { @@ -180,7 +180,7 @@ func (e *executor) run(args ...string) error { return e.CLI.Run(args) } -func (e *executor) checkTxPersisted(t *testing.T) { +func (e *executor) checkTxPersisted(t *testing.T) (*transaction.Transaction, uint32) { line, err := e.Out.ReadString('\n') require.NoError(t, err) @@ -188,10 +188,11 @@ func (e *executor) checkTxPersisted(t *testing.T) { h, err := util.Uint256DecodeStringLE(line) require.NoError(t, err, "can't decode tx hash: %s", line) - tx := e.GetTransaction(t, h) + tx, height := e.GetTransaction(t, h) aer, err := e.Chain.GetAppExecResult(tx.Hash()) require.NoError(t, err) require.Equal(t, vm.HaltState, aer.VMState) + return tx, height } func generateKeys(t *testing.T, n int) ([]*keys.PrivateKey, keys.PublicKeys) { diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index e8f8e2389..c6e7ca55c 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -12,6 +12,7 @@ import ( "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -232,13 +233,8 @@ func claimGas(ctx *cli.Context) error { if err != nil { return err } - // Temporary. - neoHash, err := util.Uint160DecodeStringLE("3b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c") - if err != nil { - return cli.NewExitError(err, 1) - } - hash, err := c.TransferNEP5(acc, scriptHash, neoHash, 0, 0) + hash, err := c.TransferNEP5(acc, scriptHash, client.NeoContractHash, 0, 0) if err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet_test.go b/cli/wallet_test.go index de9649684..56edbf15a 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/hex" + "math/big" "os" "path" "strings" @@ -163,3 +164,24 @@ func TestWalletExport(t *testing.T) { require.Equal(t, validatorWIF, strings.TrimSpace(line)) }) } + +func TestClaimGas(t *testing.T) { + e := newExecutor(t, true) + defer e.Close(t) + + start := e.Chain.BlockHeight() + balanceBefore := e.Chain.GetUtilityTokenBalance(validatorHash) + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "claim", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, + "--address", validatorAddr) + tx, end := e.checkTxPersisted(t) + b, _ := e.Chain.GetGoverningTokenBalance(validatorHash) + cl := e.Chain.CalculateClaimable(b, start, end) + require.True(t, cl.Sign() > 0) + cl.Sub(cl, big.NewInt(tx.NetworkFee+tx.SystemFee)) + + balanceAfter := e.Chain.GetUtilityTokenBalance(validatorHash) + require.Equal(t, 0, balanceAfter.Cmp(balanceBefore.Add(balanceBefore, cl))) +} From 40a24bad646d7ef6aa16fc79d9f458804b8c5453 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Sep 2020 12:20:42 +0300 Subject: [PATCH 16/20] cli: add tests for `wallet import-deployed` Fix bugs with import, allow to sign tx with contract. --- cli/smartcontract/smart_contract.go | 4 +- cli/testdata/verify.go | 5 ++ cli/testdata/verify.manifest.json | 1 + cli/testdata/verify.nef | Bin 0 -> 82 bytes cli/wallet/wallet.go | 4 +- cli/wallet_test.go | 68 ++++++++++++++++++++++++++++ pkg/wallet/account.go | 4 ++ 7 files changed, 83 insertions(+), 3 deletions(-) create mode 100644 cli/testdata/verify.go create mode 100755 cli/testdata/verify.manifest.json create mode 100755 cli/testdata/verify.nef diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 5284630f5..f0b31b41d 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -681,7 +681,6 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { rawPass, err := input.ReadPassword(ctx.App.Writer, fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) - fmt.Fprintln(ctx.App.Writer) if err != nil { return nil, cli.NewExitError(err, 1) } @@ -750,7 +749,8 @@ func contractDeploy(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) } - fmt.Fprintf(ctx.App.Writer, "Sent deployment transaction %s for contract %s\n", txHash.StringLE(), nefFile.Header.ScriptHash.StringLE()) + fmt.Fprintf(ctx.App.Writer, "Contract: %s\n", nefFile.Header.ScriptHash.StringLE()) + fmt.Fprintln(ctx.App.Writer, txHash.StringLE()) return nil } diff --git a/cli/testdata/verify.go b/cli/testdata/verify.go new file mode 100644 index 000000000..467e1fd59 --- /dev/null +++ b/cli/testdata/verify.go @@ -0,0 +1,5 @@ +package testdata + +func Verify() bool { + return true +} diff --git a/cli/testdata/verify.manifest.json b/cli/testdata/verify.manifest.json new file mode 100755 index 000000000..970102f7c --- /dev/null +++ b/cli/testdata/verify.manifest.json @@ -0,0 +1 @@ +{"abi":{"hash":"0x8dff9f223e4622961f410c015dd37052a59892bb","methods":[{"name":"verify","offset":0,"parameters":[],"returntype":"Boolean"}],"events":[]},"groups":[],"features":{"payable":true,"storage":false},"permissions":[{"contract":"*","methods":"*"}],"supportedstandards":[],"trusts":[],"safemethods":[],"extra":null} \ No newline at end of file diff --git a/cli/testdata/verify.nef b/cli/testdata/verify.nef new file mode 100755 index 0000000000000000000000000000000000000000..65f2011fb8ebd648989dcc76bfbb2734d495ed08 GIT binary patch literal 82 zcmeZsbu-RO&DTxOXTS@hfwGKH6}uBGcpJ| F007i?4%`3$ literal 0 HcmV?d00001 diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index c6e7ca55c..2b3c98532 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -407,7 +407,7 @@ func importDeployed(ctx *cli.Context) error { defer wall.Close() - rawHash := strings.TrimPrefix("0x", ctx.String("contract")) + rawHash := strings.TrimPrefix(ctx.String("contract"), "0x") h, err := util.Uint160DecodeStringLE(rawHash) if err != nil { return cli.NewExitError(fmt.Errorf("invalid contract hash: %w", err), 1) @@ -434,7 +434,9 @@ func importDeployed(ctx *cli.Context) error { if md == nil { return cli.NewExitError("contract has no `verify` method", 1) } + acc.Address = address.Uint160ToString(cs.ScriptHash()) acc.Contract.Script = cs.Script + acc.Contract.Parameters = acc.Contract.Parameters[:0] for _, p := range md.Parameters { acc.Contract.Parameters = append(acc.Contract.Parameters, wallet.ContractParam{ Name: p.Name, diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 56edbf15a..8973eb7ad 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -10,7 +10,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -185,3 +187,69 @@ func TestClaimGas(t *testing.T) { balanceAfter := e.Chain.GetUtilityTokenBalance(validatorHash) require.Equal(t, 0, balanceAfter.Cmp(balanceBefore.Add(balanceBefore, cl))) } + +func TestImportDeployed(t *testing.T) { + e := newExecutor(t, true) + defer e.Close(t) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "contract", "deploy", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, "--address", validatorAddr, + "--in", "testdata/verify.nef", "--manifest", "testdata/verify.manifest.json") + + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: ")) + h, err := util.Uint160DecodeStringLE(line) + require.NoError(t, err) + + e.checkTxPersisted(t) + + tmpDir := os.TempDir() + walletPath := path.Join(tmpDir, "wallet.json") + e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath) + defer os.Remove(walletPath) + + priv, err := keys.NewPrivateKey() + require.NoError(t, err) + + e.In.WriteString("acc\rpass\rpass\r") + e.Run(t, "neo-go", "wallet", "import-deployed", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", walletPath, "--wif", priv.WIF(), + "--contract", h.StringLE()) + + w, err := wallet.NewWalletFromFile(walletPath) + defer w.Close() + require.NoError(t, err) + require.Equal(t, 1, len(w.Accounts)) + contractAddr := w.Accounts[0].Address + require.Equal(t, address.Uint160ToString(h), contractAddr) + require.True(t, w.Accounts[0].Contract.Deployed) + + t.Run("Sign", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "nep5", "multitransfer", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, "--from", validatorAddr, + "neo:"+contractAddr+":10", + "gas:"+contractAddr+":10") + e.checkTxPersisted(t) + + privTo, err := keys.NewPrivateKey() + require.NoError(t, err) + + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "nep5", "transfer", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", walletPath, "--from", contractAddr, + "--to", privTo.Address(), "--token", "neo", "--amount", "1") + e.checkTxPersisted(t) + + b, _ := e.Chain.GetGoverningTokenBalance(h) + require.Equal(t, big.NewInt(9), b) + b, _ = e.Chain.GetGoverningTokenBalance(privTo.GetScriptHash()) + require.Equal(t, big.NewInt(1), b) + }) +} diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index 393bf22a7..7b871759b 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -98,6 +98,10 @@ func (a *Account) SignTx(t *transaction.Transaction) error { if a.privateKey == nil { return errors.New("account is not unlocked") } + if len(a.Contract.Parameters) == 0 { + t.Scripts = append(t.Scripts, transaction.Witness{}) + return nil + } data := t.GetSignedPart() if data == nil { return errors.New("failed to get transaction's signed part") From fd98a9194ac73a53043ab092ea183a1b61f3e473 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Sep 2020 13:17:13 +0300 Subject: [PATCH 17/20] cli: add tests for `wallet dump` --- cli/wallet_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cli/wallet_test.go b/cli/wallet_test.go index 8973eb7ad..b2707f257 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/hex" + "encoding/json" "math/big" "os" "path" @@ -253,3 +254,15 @@ func TestImportDeployed(t *testing.T) { require.Equal(t, big.NewInt(1), b) }) } + +func TestWalletDump(t *testing.T) { + e := newExecutor(t, false) + defer e.Close(t) + + e.Run(t, "neo-go", "wallet", "dump", "--wallet", "testdata/testwallet.json") + rawStr := strings.TrimSpace(e.Out.String()) + w := new(wallet.Wallet) + require.NoError(t, json.Unmarshal([]byte(rawStr), w)) + require.Equal(t, 1, len(w.Accounts)) + require.Equal(t, "NNuJqXDnRqvwgzhSzhH4jnVFWB1DyZ34EM", w.Accounts[0].Address) +} From 914b12af44e2fbf83322536b7d3f33a00e7d3855 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Wed, 2 Sep 2020 13:21:24 +0300 Subject: [PATCH 18/20] network: fix datarace in TCPTransport --- pkg/network/tcp_transport.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/network/tcp_transport.go b/pkg/network/tcp_transport.go index 7bf0e39eb..3a7a7a1b8 100644 --- a/pkg/network/tcp_transport.go +++ b/pkg/network/tcp_transport.go @@ -79,9 +79,11 @@ func (t *TCPTransport) isCloseError(err error) bool { // Close implements the Transporter interface. func (t *TCPTransport) Close() { + t.lock.Lock() if t.listener != nil { t.listener.Close() } + t.lock.Unlock() } // Proto implements the Transporter interface. From e0f406fd3a0b19cf785fef3e3a4890e1b4696de2 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 4 Sep 2020 16:03:31 +0300 Subject: [PATCH 19/20] rpc/client: add constant to ValidUntilBlock There are 2 problems: 1. `getvalidators` RPC can return empty list. 2. `+1` in single node can be too resrictive. Proper solution for (1) may require requesting standby validators. Here we add constant to fix occasional test failures. --- pkg/rpc/client/rpc.go | 2 +- pkg/rpc/client/rpc_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index d0bca2474..0d8ca01ff 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -489,7 +489,7 @@ func (c *Client) CalculateValidUntilBlock() (uint32, error) { expiresAt: blockCount + cacheTimeout, } } - return blockCount + validatorsCount, nil + return blockCount + validatorsCount + 1, nil } // AddNetworkFee adds network fee for each witness script and optional extra diff --git a/pkg/rpc/client/rpc_test.go b/pkg/rpc/client/rpc_test.go index 5845b3065..8fea9d613 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -1321,14 +1321,14 @@ func TestCalculateValidUntilBlock(t *testing.T) { validUntilBlock, err := c.CalculateValidUntilBlock() assert.NoError(t, err) - assert.Equal(t, uint32(54), validUntilBlock) + assert.Equal(t, uint32(55), validUntilBlock) assert.Equal(t, 1, getBlockCountCalled) assert.Equal(t, 1, getValidatorsCalled) // check, whether caching is working validUntilBlock, err = c.CalculateValidUntilBlock() assert.NoError(t, err) - assert.Equal(t, uint32(54), validUntilBlock) + assert.Equal(t, uint32(55), validUntilBlock) assert.Equal(t, 2, getBlockCountCalled) assert.Equal(t, 1, getValidatorsCalled) } From bff67c921a3a4aa96492cb4f4e982d00bc77586a Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 4 Sep 2020 16:45:31 +0300 Subject: [PATCH 20/20] core: wrap dao in `GetTestVM` MPT must not be shared to the test VM. Fix DATA RACE between `Collapse()` and RPC calls. --- pkg/core/blockchain.go | 4 +++- pkg/core/dao/dao.go | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 13266c65c..f0b4650e2 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1455,7 +1455,9 @@ func (bc *Blockchain) GetEnrollments() ([]state.Validator, error) { // GetTestVM returns a VM and a Store setup for a test run of some sort of code. func (bc *Blockchain) GetTestVM(tx *transaction.Transaction) *vm.VM { - systemInterop := bc.newInteropContext(trigger.Application, bc.dao, nil, tx) + d := bc.dao.GetWrapped().(*dao.Simple) + d.MPT = nil + systemInterop := bc.newInteropContext(trigger.Application, d, nil, tx) vm := systemInterop.SpawnVM() vm.SetPriceGetter(getPrice) return vm diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go index 22030a974..af746cef4 100644 --- a/pkg/core/dao/dao.go +++ b/pkg/core/dao/dao.go @@ -367,8 +367,10 @@ func (dao *Simple) PutStorageItem(id int32, key []byte, si *state.StorageItem) e return buf.Err } v := buf.Bytes() - if err := dao.MPT.Put(stKey[1:], v); err != nil && err != mpt.ErrNotFound { - return err + if dao.MPT != nil { + if err := dao.MPT.Put(stKey[1:], v); err != nil && err != mpt.ErrNotFound { + return err + } } return dao.Store.Put(stKey, v) } @@ -377,8 +379,10 @@ func (dao *Simple) PutStorageItem(id int32, key []byte, si *state.StorageItem) e // given key from the store. func (dao *Simple) DeleteStorageItem(id int32, key []byte) error { stKey := makeStorageItemKey(id, key) - if err := dao.MPT.Delete(stKey[1:]); err != nil && err != mpt.ErrNotFound { - return err + if dao.MPT != nil { + if err := dao.MPT.Delete(stKey[1:]); err != nil && err != mpt.ErrNotFound { + return err + } } return dao.Store.Delete(stKey) }