diff --git a/cli/candidate_test.go b/cli/candidate_test.go index d0ca3e46a..91ba7cb50 100644 --- a/cli/candidate_test.go +++ b/cli/candidate_test.go @@ -3,6 +3,7 @@ package main import ( "encoding/hex" "math/big" + "strconv" "testing" "github.com/stretchr/testify/require" @@ -48,7 +49,7 @@ func TestRegisterCandidate(t *testing.T) { "--wallet", validatorWallet, "--address", validatorPriv.Address(), "--candidate", hex.EncodeToString(validatorPriv.PublicKey().Bytes())) - e.checkTxPersisted(t) + _, index := e.checkTxPersisted(t) vs, err = e.Chain.GetEnrollments() require.Equal(t, 1, len(vs)) @@ -56,18 +57,36 @@ func TestRegisterCandidate(t *testing.T) { b, _ := e.Chain.GetGoverningTokenBalance(validatorPriv.GetScriptHash()) require.Equal(t, b, vs[0].Votes) + // check state + e.Run(t, "neo-go", "wallet", "candidate", "getstate", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--address", validatorPriv.Address()) + e.checkNextLine(t, "^\\s*Voted:\\s+"+validatorPriv.Address()) + e.checkNextLine(t, "^\\s*Amount\\s*:\\s*"+b.String()+"$") + e.checkNextLine(t, "^\\s*Block\\s*:\\s*"+strconv.FormatUint(uint64(index), 10)) + e.checkEOF(t) + // unvote e.In.WriteString("one\r") e.Run(t, "neo-go", "wallet", "candidate", "vote", "--rpc-endpoint", "http://"+e.RPC.Addr, "--wallet", validatorWallet, "--address", validatorPriv.Address()) - e.checkTxPersisted(t) + _, index = e.checkTxPersisted(t) vs, err = e.Chain.GetEnrollments() require.Equal(t, 1, len(vs)) require.Equal(t, validatorPriv.PublicKey(), vs[0].Key) require.Equal(t, big.NewInt(0), vs[0].Votes) + + // check state + e.Run(t, "neo-go", "wallet", "candidate", "getstate", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--address", validatorPriv.Address()) + e.checkNextLine(t, "^\\s*Voted:\\s+"+"null") // no vote. + e.checkNextLine(t, "^\\s*Amount\\s*:\\s*"+b.String()+"$") + e.checkNextLine(t, "^\\s*Block\\s*:\\s*"+strconv.FormatUint(uint64(index), 10)) + e.checkEOF(t) }) // missing address @@ -84,4 +103,7 @@ func TestRegisterCandidate(t *testing.T) { vs, err = e.Chain.GetEnrollments() require.Equal(t, 0, len(vs)) + + // getstate: missing address + e.RunWithError(t, "neo-go", "wallet", "candidate", "getstate") } diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 1e2475400..c94a9c648 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -7,11 +7,14 @@ import ( "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" + "github.com/nspcc-dev/neo-go/pkg/core/state" "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/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/rpc/client" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -71,6 +74,18 @@ func newValidatorCommands() []cli.Command { }, }, options.RPC...), }, + { + Name: "getstate", + Usage: "print NEO holder account state", + UsageText: "getstate -a ", + Action: getAccountState, + Flags: append([]cli.Flag{ + flags.AddressFlag{ + Name: "address, a", + Usage: "Address to get state of", + }, + }, options.RPC...), + }, } } @@ -204,3 +219,54 @@ func getDecryptedAccount(ctx *cli.Context, wall *wallet.Wallet, addr util.Uint16 } return acc, nil } + +func getAccountState(ctx *cli.Context) error { + addrFlag := ctx.Generic("address").(*flags.Address) + if !addrFlag.IsSet { + return cli.NewExitError("address was not provided", 1) + } + + gctx, cancel := options.GetTimeoutContext(ctx) + defer cancel() + c, exitErr := options.GetRPCClient(gctx, ctx) + if exitErr != nil { + return exitErr + } + + neoHash, err := c.GetNativeContractHash(nativenames.Neo) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to get NEO contract hash: %w", err), 1) + } + res, err := c.InvokeFunction(neoHash, "getAccountState", []smartcontract.Parameter{ + { + Type: smartcontract.Hash160Type, + Value: addrFlag.Uint160(), + }, + }, nil) + if err != nil { + return cli.NewExitError(err, 1) + } + if res.State != "HALT" { + return cli.NewExitError(fmt.Errorf("invocation failed: %s", res.FaultException), 1) + } + if len(res.Stack) == 0 { + return cli.NewExitError("result stack is empty", 1) + } + st := new(state.NEOBalanceState) + err = st.FromStackItem(res.Stack[0]) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to convert account state from stackitem: %w", err), 1) + } + dec, err := c.NEP17Decimals(neoHash) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to get decimals: %w", err), 1) + } + voted := "null" + if st.VoteTo != nil { + voted = address.Uint160ToString(st.VoteTo.GetScriptHash()) + } + fmt.Fprintf(ctx.App.Writer, "\tVoted: %s\n", voted) + fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", fixedn.ToString(&st.Balance, int(dec))) + fmt.Fprintf(ctx.App.Writer, "\tBlock: %d\n", st.BalanceHeight) + return nil +} diff --git a/pkg/core/state/native_state.go b/pkg/core/state/native_state.go index 4b0019da7..4b45239ab 100644 --- a/pkg/core/state/native_state.go +++ b/pkg/core/state/native_state.go @@ -2,6 +2,8 @@ package state import ( "crypto/elliptic" + "errors" + "fmt" "math/big" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" @@ -105,7 +107,7 @@ func (s *NEOBalanceState) DecodeBinary(r *io.BinReader) { if r.Err != nil { return } - r.Err = s.fromStackItem(si) + r.Err = s.FromStackItem(si) } func (s *NEOBalanceState) toStackItem() stackitem.Item { @@ -119,21 +121,33 @@ func (s *NEOBalanceState) toStackItem() stackitem.Item { return result } -func (s *NEOBalanceState) fromStackItem(item stackitem.Item) error { - structItem := item.Value().([]stackitem.Item) - s.Balance = *structItem[0].Value().(*big.Int) - s.BalanceHeight = uint32(structItem[1].Value().(*big.Int).Int64()) +// FromStackItem converts stackitem.Item to NEOBalanceState. +func (s *NEOBalanceState) FromStackItem(item stackitem.Item) error { + structItem, ok := item.Value().([]stackitem.Item) + if !ok || len(structItem) < 3 { + return errors.New("invalid stackitem length") + } + balance, err := structItem[0].TryInteger() + if err != nil { + return fmt.Errorf("invalid balance stackitem: %w", err) + } + s.Balance = *balance + h, err := structItem[1].TryInteger() + if err != nil { + return fmt.Errorf("invalid heigh stackitem") + } + s.BalanceHeight = uint32(h.Int64()) if _, ok := structItem[2].(stackitem.Null); ok { s.VoteTo = nil return nil } bs, err := structItem[2].TryBytes() if err != nil { - return err + return fmt.Errorf("invalid public key stackitem: %w", err) } pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256()) if err != nil { - return err + return fmt.Errorf("invalid public key bytes: %w", err) } s.VoteTo = pub return nil