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/executor_test.go b/cli/executor_test.go new file mode 100644 index 000000000..2a753d02b --- /dev/null +++ b/cli/executor_test.go @@ -0,0 +1,208 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "io" + "io/ioutil" + "strings" + "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/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" + "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" + "go.uber.org/zap/zaptest" + "golang.org/x/crypto/ssh/terminal" +) + +const ( + validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" + validatorAddr = "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK" + + validatorWallet = "testdata/wallet1_solo.json" +) + +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. +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 + // In contains command input. + In *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), + 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) + } + 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() + } +} + +// 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, uint32) { + var tx *transaction.Transaction + var height uint32 + require.Eventually(t, func() bool { + 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, height +} + +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) +} + +func (e *executor) checkTxPersisted(t *testing.T) (*transaction.Transaction, uint32) { + 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, 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) { + 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/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/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/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 new file mode 100644 index 000000000..2f879ecc3 --- /dev/null +++ b/cli/nep5_test.go @@ -0,0 +1,171 @@ +package main + +import ( + "io" + "math/big" + "os" + "path" + "strconv" + "testing" + + "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/wallet" + "github.com/stretchr/testify/require" +) + +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 +} + +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...) + e.checkTxPersisted(t) + + sh, err := address.StringToUint160(w.Accounts[0].Address) + require.NoError(t, err) + b, _ := e.Chain.GetGoverningTokenBalance(sh) + require.Equal(t, big.NewInt(1), b) +} + +func TestNEP5MultiTransfer(t *testing.T) { + privs, _ := generateKeys(t, 3) + + 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...) + e.checkTxPersisted(t) + + 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) +} + +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/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 } diff --git a/cli/server/server.go b/cli/server/server.go index 3f7170a8b..b4a874963 100644 --- a/cli/server/server.go +++ b/cli/server/server.go @@ -355,11 +355,11 @@ func startServer(ctx *cli.Context) error { errChan := make(chan error) go serv.Start(errChan) - go rpcServer.Start(errChan) + 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..f0b31b41d 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" ) @@ -366,7 +365,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 +404,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 +494,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 +598,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 +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.Printf("Enter account %s password > ", address.Uint160ToString(addr)) - rawPass, err := terminal.ReadPassword(syscall.Stdin) - fmt.Println() + rawPass, err := input.ReadPassword(ctx.App.Writer, + fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) if err != nil { return nil, cli.NewExitError(err, 1) } @@ -751,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.Printf("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/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/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 000000000..65f2011fb Binary files /dev/null and b/cli/testdata/verify.nef differ 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/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..58cf2012c 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -50,7 +50,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 +59,7 @@ func signMultisig(ctx *cli.Context) error { if !ok { return cli.NewExitError("verifiable item is not a transaction", 1) } - printTxInfo(tx) + fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) priv := acc.PrivateKey() sign := priv.Sign(tx.GetSignedPart()) @@ -86,11 +86,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 } @@ -115,7 +115,3 @@ func writeParameterContext(c *context.ParameterContext, filename string) error { } return nil } - -func printTxInfo(t *transaction.Transaction) { - fmt.Printf("Hash: %s\n", t.Hash().StringLE()) -} diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index 641d13696..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, @@ -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 } @@ -313,16 +314,12 @@ 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(wall, name) + token, err := getMatchingToken(ctx, wall, ctx.String("token")) 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 +340,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 +366,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, ss[0]) if err != nil { return cli.NewExitError(err, 1) } @@ -406,7 +403,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 +418,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 +467,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..671f16d8f 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -4,7 +4,9 @@ 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/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" @@ -80,10 +82,11 @@ 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() - acc, err := getDecryptedAccount(wall, addr) + acc, err := getDecryptedAccount(ctx, wall, addr) if err != nil { return cli.NewExitError(err, 1) } @@ -100,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 { @@ -111,7 +117,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 } @@ -120,10 +126,11 @@ 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() - acc, err := getDecryptedAccount(wall, addr) + acc, err := getDecryptedAccount(ctx, wall, addr) if err != nil { return cli.NewExitError(err, 1) } @@ -155,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) } @@ -168,17 +178,18 @@ 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 := 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 54c5e4f69..2b3c98532 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -1,23 +1,22 @@ package wallet import ( - "bufio" "encoding/hex" "errors" "fmt" - "os" + "io" "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" + "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" "github.com/urfave/cli" - "golang.org/x/crypto/ssh/terminal" ) var ( @@ -222,7 +221,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) } @@ -234,18 +233,13 @@ func claimGas(ctx *cli.Context) error { if err != nil { return err } - // Temporary. - neoHash, err := util.Uint160DecodeStringLE("3b7d3711c6f0ccf9b1dca903d1bfa1d896f1238c") + + hash, err := c.TransferNEP5(acc, scriptHash, client.NeoContractHash, 0, 0) if err != nil { return cli.NewExitError(err, 1) } - hash, err := c.TransferNEP5(acc, scriptHash, neoHash, 0, 0) - if err != nil { - return cli.NewExitError(err, 1) - } - - fmt.Println(hash.StringLE()) + fmt.Fprintln(ctx.App.Writer, hash.StringLE()) return nil } @@ -265,7 +259,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 := 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 { @@ -347,7 +341,7 @@ loop: for _, wif := range wifs { if decrypt { - pass, err := readPassword("Enter password > ") + pass, err := input.ReadPassword(ctx.App.Writer, "Enter password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -360,7 +354,7 @@ loop: wif = pk.WIF() } - fmt.Println(wif) + fmt.Fprintln(ctx.App.Writer, wif) } return nil @@ -389,7 +383,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) } @@ -413,13 +407,13 @@ 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) } - acc, err := newAccountFromWIF(ctx.String("wif")) + acc, err := newAccountFromWIF(ctx.App.Writer, ctx.String("wif")) if err != nil { return cli.NewExitError(err, 1) } @@ -440,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, @@ -463,7 +459,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) } @@ -476,7 +472,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) } @@ -502,8 +500,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,17 +514,15 @@ func removeAccount(ctx *cli.Context) error { return nil } -func askForConsent() bool { - fmt.Print("Are you sure? [y/N]: ") - reader := bufio.NewReader(os.Stdin) - response, err := reader.ReadString('\n') +func askForConsent(w io.Writer) bool { + response, err := input.ReadLine(w, "Are you sure? [y/N]: ") if err == nil { response = strings.ToLower(strings.TrimSpace(response)) if response == "y" || response == "yes" { return true } } - fmt.Println("Cancelled.") + fmt.Fprintln(w, "Cancelled.") return false } @@ -536,7 +532,7 @@ func dumpWallet(ctx *cli.Context) error { return cli.NewExitError(err, 1) } if ctx.Bool("decrypt") { - pass, err := readPassword("Enter wallet password > ") + pass, err := input.ReadPassword(ctx.App.Writer, "Enter wallet password > ") if err != nil { return cli.NewExitError(err, 1) } @@ -548,7 +544,7 @@ func dumpWallet(ctx *cli.Context) error { } } } - fmtPrintWallet(wall) + fmtPrintWallet(ctx.App.Writer, wall) return nil } @@ -571,20 +567,18 @@ 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) { - buf := bufio.NewReader(os.Stdin) - fmt.Print("Enter the name of the account > ") - rawName, _ := buf.ReadBytes('\n') - phrase, err := readPassword("Enter passphrase > ") +func readAccountInfo(w io.Writer) (string, string, error) { + 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("Confirm passphrase > ") + phraseCheck, err := input.ReadPassword(w, "Confirm passphrase > ") if err != nil { return "", "", err } @@ -598,7 +592,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 +606,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 := input.ReadPassword(w, "Enter password > ") if err != nil { return nil, err } @@ -629,8 +623,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 +648,9 @@ func addAccountAndSave(w *wallet.Wallet, acc *wallet.Account) error { return w.Save() } -func readPassword(prompt string) (string, error) { - fmt.Print(prompt) - rawPass, err := terminal.ReadPassword(syscall.Stdin) - fmt.Println() - 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, "") } diff --git a/cli/wallet_test.go b/cli/wallet_test.go new file mode 100644 index 000000000..b2707f257 --- /dev/null +++ b/cli/wallet_test.go @@ -0,0 +1,268 @@ +package main + +import ( + "encoding/hex" + "encoding/json" + "math/big" + "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/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" +) + +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) + 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) { + 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")) + }) + t.Run("Multisig", func(t *testing.T) { + privs, pubs := generateKeys(t, 4) + + 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) + }) + }) +} + +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)) + }) +} + +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))) +} + +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) + }) +} + +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) +} 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 diff --git a/pkg/consensus/consensus.go b/pkg/consensus/consensus.go index 024f74d4b..05cfd780d 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/core/blockchain.go b/pkg/core/blockchain.go index 449dca250..527af9b49 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -1420,7 +1420,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) } 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) } 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. 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 f8cb2f87f..01e04b077 100644 --- a/pkg/rpc/client/rpc_test.go +++ b/pkg/rpc/client/rpc_test.go @@ -1327,14 +1327,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) } 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) 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")