From 37619743ada57119420cd69454f53bfb17ae5f77 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Tue, 16 Aug 2022 22:32:46 +0300 Subject: [PATCH 1/4] unwrap: add ArrayOfPublicKeys() This type of result is also popular in the NEO contract. --- pkg/rpcclient/rolemgmt/roles.go | 20 +------------------- pkg/rpcclient/unwrap/unwrap.go | 23 +++++++++++++++++++++++ pkg/rpcclient/unwrap/unwrap_test.go | 23 +++++++++++++++++++++++ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/pkg/rpcclient/rolemgmt/roles.go b/pkg/rpcclient/rolemgmt/roles.go index d153feb9c..c7593e4b5 100644 --- a/pkg/rpcclient/rolemgmt/roles.go +++ b/pkg/rpcclient/rolemgmt/roles.go @@ -7,9 +7,6 @@ various methods to perform the only RoleManagement state-changing call. package rolemgmt import ( - "crypto/elliptic" - "fmt" - "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/core/state" @@ -78,22 +75,7 @@ func New(actor Actor) *Contract { // given role at the given height. The list can be empty if no keys are // configured for this role/height. func (c *ContractReader) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.PublicKeys, error) { - arr, err := unwrap.Array(c.invoker.Call(Hash, "getDesignatedByRole", int64(role), index)) - if err != nil { - return nil, err - } - pks := make(keys.PublicKeys, len(arr)) - for i, item := range arr { - val, err := item.TryBytes() - if err != nil { - return nil, fmt.Errorf("invalid array element #%d: %s", i, item.Type()) - } - pks[i], err = keys.NewPublicKeyFromBytes(val, elliptic.P256()) - if err != nil { - return nil, err - } - } - return pks, nil + return unwrap.ArrayOfPublicKeys(c.invoker.Call(Hash, "getDesignatedByRole", int64(role), index)) } // DesignateAsRole creates and sends a transaction that sets the keys used for diff --git a/pkg/rpcclient/unwrap/unwrap.go b/pkg/rpcclient/unwrap/unwrap.go index 208b041c1..de82e6121 100644 --- a/pkg/rpcclient/unwrap/unwrap.go +++ b/pkg/rpcclient/unwrap/unwrap.go @@ -11,12 +11,14 @@ contract-specific packages. package unwrap import ( + "crypto/elliptic" "errors" "fmt" "math/big" "unicode/utf8" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -190,6 +192,27 @@ func ArrayOfBytes(r *result.Invoke, err error) ([][]byte, error) { return res, nil } +// ArrayOfPublicKeys checks the result for correct state (HALT) and then +// extracts a slice of public keys from the returned stack item. +func ArrayOfPublicKeys(r *result.Invoke, err error) (keys.PublicKeys, error) { + arr, err := Array(r, err) + if err != nil { + return nil, err + } + pks := make(keys.PublicKeys, len(arr)) + for i, item := range arr { + val, err := item.TryBytes() + if err != nil { + return nil, fmt.Errorf("invalid array element #%d: %s", i, item.Type()) + } + pks[i], err = keys.NewPublicKeyFromBytes(val, elliptic.P256()) + if err != nil { + return nil, fmt.Errorf("array element #%d in not a key: %w", i, err) + } + } + return pks, nil +} + // Map expects correct execution (HALT state) with a single stack item // returned. A stackitem.Map is extracted from this item and returned. func Map(r *result.Invoke, err error) (*stackitem.Map, error) { diff --git a/pkg/rpcclient/unwrap/unwrap_test.go b/pkg/rpcclient/unwrap/unwrap_test.go index e62383239..60fc60088 100644 --- a/pkg/rpcclient/unwrap/unwrap_test.go +++ b/pkg/rpcclient/unwrap/unwrap_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" @@ -52,6 +53,9 @@ func TestStdErrors(t *testing.T) { func(r *result.Invoke, err error) (interface{}, error) { return ArrayOfBytes(r, err) }, + func(r *result.Invoke, err error) (interface{}, error) { + return ArrayOfPublicKeys(r, err) + }, func(r *result.Invoke, err error) (interface{}, error) { return Map(r, err) }, @@ -224,6 +228,25 @@ func TestArrayOfBytes(t *testing.T) { require.Equal(t, []byte("some"), a[0]) } +func TestArrayOfPublicKeys(t *testing.T) { + _, err := ArrayOfPublicKeys(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) + require.Error(t, err) + + _, err = ArrayOfPublicKeys(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make([]stackitem.Item{})})}}, nil) + require.Error(t, err) + + _, err = ArrayOfPublicKeys(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make([]byte("some"))})}}, nil) + require.Error(t, err) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + pks, err := ArrayOfPublicKeys(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make([]stackitem.Item{stackitem.Make(k.PublicKey().Bytes())})}}, nil) + require.NoError(t, err) + require.Equal(t, 1, len(pks)) + require.Equal(t, k.PublicKey(), pks[0]) +} + func TestMap(t *testing.T) { _, err := Map(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) require.Error(t, err) From 689331b9607c7bdf455eceff30e21b77fd0ce369 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 17 Aug 2022 15:08:24 +0300 Subject: [PATCH 2/4] unwrap: perform consistency check in SessionIterator C# servers with SessionEnabled=false will return iterator IDs and no session IDs which can be reported as an error immediately because the iterator can't be traversed. --- pkg/rpcclient/unwrap/unwrap.go | 3 +++ pkg/rpcclient/unwrap/unwrap_test.go | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pkg/rpcclient/unwrap/unwrap.go b/pkg/rpcclient/unwrap/unwrap.go index de82e6121..fd1d27a90 100644 --- a/pkg/rpcclient/unwrap/unwrap.go +++ b/pkg/rpcclient/unwrap/unwrap.go @@ -155,6 +155,9 @@ func SessionIterator(r *result.Invoke, err error) (uuid.UUID, result.Iterator, e if !ok { return uuid.UUID{}, result.Iterator{}, errors.New("the item is InteropInterface, but not an Iterator") } + if (r.Session == uuid.UUID{}) && iter.ID != nil { + return uuid.UUID{}, result.Iterator{}, errors.New("server returned iterator ID, but no session ID") + } return r.Session, iter, nil } diff --git a/pkg/rpcclient/unwrap/unwrap_test.go b/pkg/rpcclient/unwrap/unwrap_test.go index 60fc60088..2f895a07e 100644 --- a/pkg/rpcclient/unwrap/unwrap_test.go +++ b/pkg/rpcclient/unwrap/unwrap_test.go @@ -197,8 +197,11 @@ func TestSessionIterator(t *testing.T) { require.Error(t, err) iid := uuid.New() - sid := uuid.New() iter := result.Iterator{ID: &iid} + _, _, err = SessionIterator(&result.Invoke{State: "HALT", Stack: []stackitem.Item{stackitem.NewInterop(iter)}}, nil) + require.Error(t, err) + + sid := uuid.New() rs, ri, err := SessionIterator(&result.Invoke{Session: sid, State: "HALT", Stack: []stackitem.Item{stackitem.NewInterop(iter)}}, nil) require.NoError(t, err) require.Equal(t, sid, rs) From 79051f21c186d543dc51f29cd2a2b519289d8b7c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 17 Aug 2022 16:38:03 +0300 Subject: [PATCH 3/4] invoker: expand the interface with iterator wrappers We need them for iterator-based interfaces. Invoker hides the difference between different (session/expanded) iterators here. --- pkg/rpcclient/actor/actor_test.go | 9 +++- pkg/rpcclient/invoker/invoker.go | 73 +++++++++++++++++++++++++++ pkg/rpcclient/invoker/invoker_test.go | 59 +++++++++++++++++++++- 3 files changed, 139 insertions(+), 2 deletions(-) diff --git a/pkg/rpcclient/actor/actor_test.go b/pkg/rpcclient/actor/actor_test.go index 46cdd3906..32e9eca00 100644 --- a/pkg/rpcclient/actor/actor_test.go +++ b/pkg/rpcclient/actor/actor_test.go @@ -4,11 +4,13 @@ import ( "errors" "testing" + "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -44,7 +46,12 @@ func (r *RPCClient) GetVersion() (*result.Version, error) { func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) { return r.hash, r.err } - +func (r *RPCClient) TerminateSession(sessionID uuid.UUID) (bool, error) { + return false, nil // Just a stub, unused by actor. +} +func (r *RPCClient) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) { + return nil, nil // Just a stub, unused by actor. +} func testRPCAndAccount(t *testing.T) (*RPCClient, *wallet.Account) { client := &RPCClient{ version: &result.Version{ diff --git a/pkg/rpcclient/invoker/invoker.go b/pkg/rpcclient/invoker/invoker.go index 16a13c972..266d09687 100644 --- a/pkg/rpcclient/invoker/invoker.go +++ b/pkg/rpcclient/invoker/invoker.go @@ -1,17 +1,35 @@ package invoker import ( + "errors" "fmt" + "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) +// DefaultIteratorResultItems is the default number of results to +// request from the iterator. Typically it's the same as server's +// MaxIteratorResultItems, but different servers can have different +// settings. +const DefaultIteratorResultItems = 100 + +// RPCSessions is a set of RPC methods needed to retrieve values from the +// session-based iterators. +type RPCSessions interface { + TerminateSession(sessionID uuid.UUID) (bool, error) + TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) +} + // RPCInvoke is a set of RPC methods needed to execute things at the current // blockchain height. type RPCInvoke interface { + RPCSessions + InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) @@ -20,6 +38,8 @@ type RPCInvoke interface { // RPCInvokeHistoric is a set of RPC methods needed to execute things at some // fixed point in blockchain's life. type RPCInvokeHistoric interface { + RPCSessions + InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) @@ -117,6 +137,14 @@ func (h *historicConverter) InvokeContractVerify(contract util.Uint160, params [ panic("uninitialized historicConverter") } +func (h *historicConverter) TerminateSession(sessionID uuid.UUID) (bool, error) { + return h.client.TerminateSession(sessionID) +} + +func (h *historicConverter) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) { + return h.client.TraverseIterator(sessionID, iteratorID, maxItemsCount) +} + // Call invokes a method of the contract with the given parameters (and // Invoker-specific list of signers) and returns the result as is. func (v *Invoker) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) { @@ -157,3 +185,48 @@ func (v *Invoker) Verify(contract util.Uint160, witnesses []transaction.Witness, func (v *Invoker) Run(script []byte) (*result.Invoke, error) { return v.client.InvokeScript(script, v.signers) } + +// TerminateSession closes the given session, returning an error if anything +// goes wrong. +func (v *Invoker) TerminateSession(sessionID uuid.UUID) error { + return termSession(v.client, sessionID) +} + +func termSession(rpc RPCSessions, sessionID uuid.UUID) error { + r, err := rpc.TerminateSession(sessionID) + if err != nil { + return err + } + if !r { + return errors.New("terminatesession returned false") + } + return nil +} + +// TraverseIterator allows to retrieve the next batch of items from the given +// iterator in the given session (previously returned from Call or Run). It works +// both with session-backed iterators and expanded ones (which one you have +// depends on the RPC server). It can change the state of the iterator in the +// process. If num <= 0 then DefaultIteratorResultItems number of elements is +// requested. If result contains no elements, then either Iterator has no +// elements or session was expired and terminated by the server. +func (v *Invoker) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) { + return iterateNext(v.client, sessionID, iterator, num) +} + +func iterateNext(rpc RPCSessions, sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) { + if num <= 0 { + num = DefaultIteratorResultItems + } + + if iterator.ID != nil { + return rpc.TraverseIterator(sessionID, *iterator.ID, num) + } + if num > len(iterator.Values) { + num = len(iterator.Values) + } + items := iterator.Values[:num] + iterator.Values = iterator.Values[num:] + + return items, nil +} diff --git a/pkg/rpcclient/invoker/invoker_test.go b/pkg/rpcclient/invoker/invoker_test.go index a5e59af3e..e36f1f3bb 100644 --- a/pkg/rpcclient/invoker/invoker_test.go +++ b/pkg/rpcclient/invoker/invoker_test.go @@ -1,17 +1,22 @@ package invoker import ( + "errors" "testing" + "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) type rpcInv struct { resInv *result.Invoke + resTrm bool + resItm []stackitem.Item err error } @@ -51,10 +56,16 @@ func (r *rpcInv) InvokeScriptAtHeight(height uint32, script []byte, signers []tr func (r *rpcInv) InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { return r.resInv, r.err } +func (r *rpcInv) TerminateSession(sessionID uuid.UUID) (bool, error) { + return r.resTrm, r.err +} +func (r *rpcInv) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) { + return r.resItm, r.err +} func TestInvoker(t *testing.T) { resExp := &result.Invoke{State: "HALT"} - ri := &rpcInv{resExp, nil} + ri := &rpcInv{resExp, true, nil, nil} testInv := func(t *testing.T, inv *Invoker) { res, err := inv.Call(util.Uint160{}, "method") @@ -112,4 +123,50 @@ func TestInvoker(t *testing.T) { require.Panics(t, func() { _, _ = inv.Verify(util.Uint160{}, nil, "param") }) require.Panics(t, func() { _, _ = inv.Run([]byte{1}) }) }) + t.Run("terminate session", func(t *testing.T) { + for _, inv := range []*Invoker{New(ri, nil), NewHistoricAtBlock(util.Uint256{}, ri, nil)} { + ri.err = errors.New("") + require.Error(t, inv.TerminateSession(uuid.UUID{})) + ri.err = nil + ri.resTrm = false + require.Error(t, inv.TerminateSession(uuid.UUID{})) + ri.resTrm = true + require.NoError(t, inv.TerminateSession(uuid.UUID{})) + } + }) + t.Run("traverse iterator", func(t *testing.T) { + for _, inv := range []*Invoker{New(ri, nil), NewHistoricAtBlock(util.Uint256{}, ri, nil)} { + res, err := inv.TraverseIterator(uuid.UUID{}, &result.Iterator{ + Values: []stackitem.Item{stackitem.Make(42)}, + }, 0) + require.NoError(t, err) + require.Equal(t, []stackitem.Item{stackitem.Make(42)}, res) + + res, err = inv.TraverseIterator(uuid.UUID{}, &result.Iterator{ + Values: []stackitem.Item{stackitem.Make(42)}, + }, 1) + require.NoError(t, err) + require.Equal(t, []stackitem.Item{stackitem.Make(42)}, res) + + res, err = inv.TraverseIterator(uuid.UUID{}, &result.Iterator{ + Values: []stackitem.Item{stackitem.Make(42)}, + }, 2) + require.NoError(t, err) + require.Equal(t, []stackitem.Item{stackitem.Make(42)}, res) + + ri.err = errors.New("") + _, err = inv.TraverseIterator(uuid.UUID{}, &result.Iterator{ + ID: &uuid.UUID{}, + }, 2) + require.Error(t, err) + + ri.err = nil + ri.resItm = []stackitem.Item{stackitem.Make(42)} + res, err = inv.TraverseIterator(uuid.UUID{}, &result.Iterator{ + ID: &uuid.UUID{}, + }, 2) + require.NoError(t, err) + require.Equal(t, []stackitem.Item{stackitem.Make(42)}, res) + } + }) } From f011b3c3ddceb14e2825944aeab5e704513bba39 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 17 Aug 2022 21:55:30 +0300 Subject: [PATCH 4/4] rpcclient: introduce NEO wrapper Notice that int64 types are used for gas per block or registration price because the price has to fit into the system fee limitation and gas per block value can't be more than 10 GAS. We use int64 for votes as well in other types since NEO is limited to 100M. --- cli/query/query.go | 22 +- cli/wallet/nep17.go | 21 +- cli/wallet/validator.go | 141 ++----- cli/wallet/wallet.go | 9 +- pkg/rpcclient/native.go | 4 + pkg/rpcclient/neo/neo.go | 462 +++++++++++++++++++++++ pkg/rpcclient/neo/neo_test.go | 568 +++++++++++++++++++++++++++++ pkg/services/rpcsrv/client_test.go | 130 +++++++ 8 files changed, 1201 insertions(+), 156 deletions(-) create mode 100644 pkg/rpcclient/neo/neo.go create mode 100644 pkg/rpcclient/neo/neo_test.go diff --git a/cli/query/query.go b/cli/query/query.go index 2d390e42f..1d7c4ff66 100644 --- a/cli/query/query.go +++ b/cli/query/query.go @@ -13,18 +13,15 @@ import ( "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/cli/flags" "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/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" - "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" "github.com/urfave/cli" ) @@ -287,23 +284,14 @@ func queryVoter(ctx *cli.Context) error { 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) - } - inv := invoker.New(c, nil) - neoToken := nep17.NewReader(inv, neoHash) + neoToken := neo.NewReader(invoker.New(c, nil)) - itm, err := unwrap.Item(inv.Call(neoHash, "getAccountState", addr)) + st, err := neoToken.GetAccountState(addr) if err != nil { return cli.NewExitError(err, 1) } - st := new(state.NEOBalance) - if _, ok := itm.(stackitem.Null); !ok { - err = st.FromStackItem(itm) - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to convert account state from stackitem: %w", err), 1) - } + if st == nil { + st = new(state.NEOBalance) } dec, err := neoToken.Decimals() if err != nil { diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 0ef3463dc..5adba25d0 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -12,14 +12,13 @@ import ( "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/paramcontext" - "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "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/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" "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" @@ -248,12 +247,15 @@ func getNEP17Balance(ctx *cli.Context) error { continue } if gasSymbol != name { - neoSymbol, h, err = getNativeNEP17Symbol(c, nativenames.Neo) + n := neo.NewReader(invoker.New(c, nil)) + neoSymbol, err = n.Symbol() if err != nil { continue } if neoSymbol != name { continue + } else { + h = neo.Hash } } else { h = gas.Hash @@ -287,19 +289,6 @@ func printAssetBalance(ctx *cli.Context, asset util.Uint160, tokenName, tokenSym fmt.Fprintf(ctx.App.Writer, "\tUpdated: %d\n", balance.LastUpdated) } -func getNativeNEP17Symbol(c *rpcclient.Client, name string) (string, util.Uint160, error) { - h, err := c.GetNativeContractHash(name) - if err != nil { - return "", util.Uint160{}, fmt.Errorf("failed to get native %s hash: %w", name, err) - } - nepTok := nep17.NewReader(invoker.New(c, nil), h) - symbol, err := nepTok.Symbol() - if err != nil { - return "", util.Uint160{}, fmt.Errorf("failed to get native %s symbol: %w", name, err) - } - return symbol, h, nil -} - func getMatchingToken(ctx *cli.Context, w *wallet.Wallet, name string, standard string) (*wallet.Token, error) { return getMatchingTokenAux(ctx, func(i int) *wallet.Token { return w.Extra.Tokens[i] diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 216b29252..d4e832941 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -7,15 +7,12 @@ 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/native/nativenames" "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/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" ) @@ -78,24 +75,18 @@ func newValidatorCommands() []cli.Command { } func handleRegister(ctx *cli.Context) error { - return handleCandidate(ctx, true) + return handleNeoAction(ctx, func(contract *neo.Contract, _ util.Uint160, acc *wallet.Account) (*transaction.Transaction, error) { + return contract.RegisterCandidateUnsigned(acc.PrivateKey().PublicKey()) + }) } func handleUnregister(ctx *cli.Context) error { - return handleCandidate(ctx, false) + return handleNeoAction(ctx, func(contract *neo.Contract, _ util.Uint160, acc *wallet.Account) (*transaction.Transaction, error) { + return contract.UnregisterCandidateUnsigned(acc.PrivateKey().PublicKey()) + }) } -func handleCandidate(ctx *cli.Context, register bool) error { - const ( - regMethod = "registerCandidate" - unregMethod = "unregisterCandidate" - ) - var ( - err error - script []byte - sysGas int64 - ) - +func handleNeoAction(ctx *cli.Context, mkTx func(*neo.Contract, util.Uint160, *wallet.Account) (*transaction.Transaction, error)) error { if err := cmdargs.EnsureNone(ctx); err != nil { return err } @@ -121,124 +112,42 @@ func handleCandidate(ctx *cli.Context, register bool) error { if err != nil { return cli.NewExitError(err, 1) } - act, err := actor.NewSimple(c, acc) if err != nil { return cli.NewExitError(fmt.Errorf("RPC actor issue: %w", err), 1) } gas := flags.Fixed8FromContext(ctx, "gas") - neoContractHash, err := c.GetNativeContractHash(nativenames.Neo) - if err != nil { - return err - } - unregScript, err := smartcontract.CreateCallWithAssertScript(neoContractHash, unregMethod, acc.PrivateKey().PublicKey().Bytes()) + contract := neo.New(act) + tx, err := mkTx(contract, addr, acc) if err != nil { return cli.NewExitError(err, 1) } - if !register { - script = unregScript - } else { - script, err = smartcontract.CreateCallWithAssertScript(neoContractHash, regMethod, acc.PrivateKey().PublicKey().Bytes()) - if err != nil { - return cli.NewExitError(err, 1) - } - } - // Registration price is normally much bigger than MaxGasInvoke, so to - // determine proper amount of GAS we _always_ run unreg script and then - // add registration price to it if needed. - r, err := act.Run(unregScript) + tx.NetworkFee += int64(gas) + res, _, err := act.SignAndSend(tx) if err != nil { - return cli.NewExitError(fmt.Errorf("Run failure: %w", err), 1) - } - sysGas = r.GasConsumed - if register { - // Deregistration will fail, so there is no point in checking State. - regPrice, err := c.GetCandidateRegisterPrice() - if err != nil { - return cli.NewExitError(err, 1) - } - sysGas += regPrice - } else if r.State != vmstate.Halt.String() { - return cli.NewExitError(fmt.Errorf("unregister transaction failed: %s", r.FaultException), 1) - } - res, _, err := act.SendUncheckedRun(script, sysGas, nil, func(t *transaction.Transaction) error { - t.NetworkFee += int64(gas) - return nil - }) - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to push transaction: %w", err), 1) + return cli.NewExitError(fmt.Errorf("failed to sign/send transaction: %w", err), 1) } fmt.Fprintln(ctx.App.Writer, res.StringLE()) return nil } func handleVote(ctx *cli.Context) error { - if err := cmdargs.EnsureNone(ctx); err != nil { - return err - } - wall, pass, err := readWallet(ctx) - if err != nil { - return cli.NewExitError(err, 1) - } - - addrFlag := ctx.Generic("address").(*flags.Address) - if !addrFlag.IsSet { - return cli.NewExitError("address was not provided", 1) - } - addr := addrFlag.Uint160() - acc, err := getDecryptedAccount(wall, addr, pass) - if err != nil { - return cli.NewExitError(err, 1) - } - - var pub *keys.PublicKey - pubStr := ctx.String("candidate") - if pubStr != "" { - pub, err = keys.NewPublicKeyFromString(pubStr) - if err != nil { - return cli.NewExitError(fmt.Errorf("invalid public key: '%s'", pubStr), 1) + return handleNeoAction(ctx, func(contract *neo.Contract, addr util.Uint160, acc *wallet.Account) (*transaction.Transaction, error) { + var ( + err error + pub *keys.PublicKey + ) + pubStr := ctx.String("candidate") + if pubStr != "" { + pub, err = keys.NewPublicKeyFromString(pubStr) + if err != nil { + return nil, fmt.Errorf("invalid public key: '%s'", pubStr) + } } - } - gctx, cancel := options.GetTimeoutContext(ctx) - defer cancel() - - c, err := options.GetRPCClient(gctx, ctx) - if err != nil { - return cli.NewExitError(err, 1) - } - act, err := actor.NewSimple(c, acc) - if err != nil { - return cli.NewExitError(fmt.Errorf("RPC actor issue: %w", err), 1) - } - - var pubArg interface{} - if pub != nil { - pubArg = pub.Bytes() - } - - gas := flags.Fixed8FromContext(ctx, "gas") - neoContractHash, err := c.GetNativeContractHash(nativenames.Neo) - if err != nil { - return cli.NewExitError(err, 1) - } - script, err := smartcontract.CreateCallWithAssertScript(neoContractHash, "vote", addr.BytesBE(), pubArg) - if err != nil { - return cli.NewExitError(err, 1) - } - res, _, err := act.SendTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error { - if r.State != vmstate.Halt.String() { - return fmt.Errorf("invocation failed: %s", r.FaultException) - } - t.NetworkFee += int64(gas) - return nil + return contract.VoteUnsigned(addr, pub) }) - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to push invocation transaction: %w", err), 1) - } - fmt.Fprintln(ctx.App.Writer, res.StringLE()) - return nil } // getDecryptedAccount tries to unlock the specified account. If password is nil, it will be requested via terminal. diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index 21a8d514b..42640245c 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -15,11 +15,10 @@ import ( "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/pkg/config" - "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "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/rpcclient/actor" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/vm" @@ -336,11 +335,7 @@ func claimGas(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - neoContractHash, err := c.GetNativeContractHash(nativenames.Neo) - if err != nil { - return cli.NewExitError(err, 1) - } - neoToken := nep17.New(act, neoContractHash) + neoToken := neo.New(act) hash, _, err := neoToken.Transfer(scriptHash, scriptHash, big.NewInt(0), nil) if err != nil { return cli.NewExitError(err, 1) diff --git a/pkg/rpcclient/native.go b/pkg/rpcclient/native.go index 203bc04a0..0b6526c44 100644 --- a/pkg/rpcclient/native.go +++ b/pkg/rpcclient/native.go @@ -36,11 +36,15 @@ func (c *Client) GetNNSPrice(nnsHash util.Uint160) (int64, error) { } // GetGasPerBlock invokes `getGasPerBlock` method on a native NEO contract. +// +// Deprecated: please use neo subpackage. This method will be removed in future releases. func (c *Client) GetGasPerBlock() (int64, error) { return c.getFromNEO("getGasPerBlock") } // GetCandidateRegisterPrice invokes `getRegisterPrice` method on native NEO contract. +// +// Deprecated: please use neo subpackage. This method will be removed in future releases. func (c *Client) GetCandidateRegisterPrice() (int64, error) { return c.getFromNEO("getRegisterPrice") } diff --git a/pkg/rpcclient/neo/neo.go b/pkg/rpcclient/neo/neo.go new file mode 100644 index 000000000..da2fe0d45 --- /dev/null +++ b/pkg/rpcclient/neo/neo.go @@ -0,0 +1,462 @@ +/* +Package neo provides an RPC-based wrapper for the NEOToken contract. + +Safe methods are encapsulated into ContractReader structure while Contract provides +various methods to perform state-changing calls. +*/ +package neo + +import ( + "crypto/elliptic" + "fmt" + "math/big" + + "github.com/google/uuid" + "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/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" +) + +const ( + setGasMethod = "setGasPerBlock" + setRegMethod = "setRegisterPrice" +) + +// Invoker is used by ContractReader to perform read-only calls. +type Invoker interface { + nep17.Invoker + + CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) + TerminateSession(sessionID uuid.UUID) error + TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) +} + +// Actor is used by Contract to create and send transactions. +type Actor interface { + nep17.Actor + Invoker + + Run(script []byte) (*result.Invoke, error) + MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) + MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) + MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error) + SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) + Sign(tx *transaction.Transaction) error + SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error) +} + +// ContractReader represents safe (read-only) methods of NEO. It can be +// used to query various data. +type ContractReader struct { + nep17.TokenReader + + invoker Invoker +} + +// Contract provides full NEO interface, both safe and state-changing methods. +type Contract struct { + ContractReader + nep17.Token + + actor Actor +} + +// CandidateStateEvent represents a CandidateStateChanged NEO event. +type CandidateStateEvent struct { + Key *keys.PublicKey + Registered bool + Votes *big.Int +} + +// VoteEvent represents a Vote NEO event. +type VoteEvent struct { + Account util.Uint160 + From *keys.PublicKey + To *keys.PublicKey + Amount *big.Int +} + +// ValidatorIterator is used for iterating over GetAllCandidates results. +type ValidatorIterator struct { + client Invoker + session uuid.UUID + iterator result.Iterator +} + +// Hash stores the hash of the native NEOToken contract. +var Hash = state.CreateNativeContractHash(nativenames.Neo) + +// NewReader creates an instance of ContractReader to get data from the NEO +// contract. +func NewReader(invoker Invoker) *ContractReader { + return &ContractReader{*nep17.NewReader(invoker, Hash), invoker} +} + +// New creates an instance of Contract to perform state-changing actions in the +// NEO contract. +func New(actor Actor) *Contract { + return &Contract{*NewReader(actor), *nep17.New(actor, Hash), actor} +} + +// GetAccountState returns current NEO balance state for the account which +// includes balance and voting data. It can return nil balance with no error +// if the account given has no NEO. +func (c *ContractReader) GetAccountState(account util.Uint160) (*state.NEOBalance, error) { + itm, err := unwrap.Item(c.invoker.Call(Hash, "getAccountState", account)) + if err != nil { + return nil, err + } + if _, ok := itm.(stackitem.Null); ok { + return nil, nil + } + res := new(state.NEOBalance) + err = res.FromStackItem(itm) + if err != nil { + return nil, err + } + return res, nil +} + +// GetAllCandidates returns an iterator that allows to retrieve all registered +// validators from it. It depends on the server to provide proper session-based +// iterator, but can also work with expanded one. +func (c *ContractReader) GetAllCandidates() (*ValidatorIterator, error) { + sess, iter, err := unwrap.SessionIterator(c.invoker.Call(Hash, "getAllCandidates")) + if err != nil { + return nil, err + } + + return &ValidatorIterator{ + client: c.invoker, + iterator: iter, + session: sess, + }, nil +} + +// GetAllCandidatesExpanded is similar to GetAllCandidates (uses the same NEO +// method), but can be useful if the server used doesn't support sessions and +// doesn't expand iterators. It creates a script that will get num of result +// items from the iterator right in the VM and return them to you. It's only +// limited by VM stack and GAS available for RPC invocations. +func (c *ContractReader) GetAllCandidatesExpanded(num int) ([]result.Validator, error) { + arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "getAllCandidates", num)) + if err != nil { + return nil, err + } + return itemsToValidators(arr) +} + +// Next returns the next set of elements from the iterator (up to num of them). +// It can return less than num elements in case iterator doesn't have that many +// or zero elements if the iterator has no more elements or the session is +// expired. +func (v *ValidatorIterator) Next(num int) ([]result.Validator, error) { + items, err := v.client.TraverseIterator(v.session, &v.iterator, num) + if err != nil { + return nil, err + } + return itemsToValidators(items) +} + +// Terminate closes the iterator session used by ValidatorIterator (if it's +// session-based). +func (v *ValidatorIterator) Terminate() error { + if v.iterator.ID == nil { + return nil + } + return v.client.TerminateSession(v.session) +} + +// GetCandidates returns the list of validators with their vote count. This +// method is mostly useful for historic invocations because the RPC protocol +// provides direct getcandidates call that returns more data and works faster. +// The contract only returns up to 256 candidates in response to this method, so +// if there are more of them on the network you will get a truncated result, use +// GetAllCandidates to solve this problem. +func (c *ContractReader) GetCandidates() ([]result.Validator, error) { + arr, err := unwrap.Array(c.invoker.Call(Hash, "getCandidates")) + if err != nil { + return nil, err + } + return itemsToValidators(arr) +} + +func itemsToValidators(arr []stackitem.Item) ([]result.Validator, error) { + res := make([]result.Validator, len(arr)) + for i, itm := range arr { + str, ok := itm.Value().([]stackitem.Item) + if !ok { + return nil, fmt.Errorf("item #%d is not a structure", i) + } + if len(str) != 2 { + return nil, fmt.Errorf("item #%d has wrong length", i) + } + b, err := str[0].TryBytes() + if err != nil { + return nil, fmt.Errorf("item #%d has wrong key: %w", i, err) + } + k, err := keys.NewPublicKeyFromBytes(b, elliptic.P256()) + if err != nil { + return nil, fmt.Errorf("item #%d has wrong key: %w", i, err) + } + votes, err := str[1].TryInteger() + if err != nil { + return nil, fmt.Errorf("item #%d has wrong votes: %w", i, err) + } + if !votes.IsInt64() { + return nil, fmt.Errorf("item #%d has too big number of votes", i) + } + res[i].PublicKey = *k + res[i].Votes = votes.Int64() + } + return res, nil +} + +// GetCommittee returns the list of committee member public keys. This +// method is mostly useful for historic invocations because the RPC protocol +// provides direct getcommittee call that works faster. +func (c *ContractReader) GetCommittee() (keys.PublicKeys, error) { + return unwrap.ArrayOfPublicKeys(c.invoker.Call(Hash, "getCommittee")) +} + +// GetNextBlockValidators returns the list of validator keys that will sign the +// next block. This method is mostly useful for historic invocations because the +// RPC protocol provides direct getnextblockvalidators call that provides more +// data and works faster. +func (c *ContractReader) GetNextBlockValidators() (keys.PublicKeys, error) { + return unwrap.ArrayOfPublicKeys(c.invoker.Call(Hash, "getNextBlockValidators")) +} + +// GetGasPerBlock returns the amount of GAS generated in each block. +func (c *ContractReader) GetGasPerBlock() (int64, error) { + return unwrap.Int64(c.invoker.Call(Hash, "getGasPerBlock")) +} + +// GetRegisterPrice returns the price of candidate key registration. +func (c *ContractReader) GetRegisterPrice() (int64, error) { + return unwrap.Int64(c.invoker.Call(Hash, "getRegisterPrice")) +} + +// UnclaimedGas allows to calculate the amount of GAS that will be generated if +// any NEO state change ("claim") is to happen for the given account at the given +// block number. This method is mostly useful for historic invocations because +// the RPC protocol provides direct getunclaimedgas method that works faster. +func (c *ContractReader) UnclaimedGas(account util.Uint160, end uint32) (*big.Int, error) { + return unwrap.BigInt(c.invoker.Call(Hash, "unclaimedGas", account, end)) +} + +// RegisterCandidate creates and sends a transaction that adds the given key to +// the list of candidates that can be voted for. The return result from the +// "registerCandidate" method is checked to be true, so transaction fails (with +// FAULT state) if not successful. Notice that for this call to work it must be +// witnessed by the simple account derived from the given key, so use an +// appropriate Actor. The returned values are transaction hash, its +// ValidUntilBlock value and an error if any. +// +// Notice that unlike for all other methods the script for this one is not +// test-executed in its final form because most networks have registration price +// set to be much higher than typical RPC server allows to spend during +// test-execution. This adds some risk that it might fail on-chain, but in +// practice it's not likely to happen if signers are set up correctly. +func (c *Contract) RegisterCandidate(k *keys.PublicKey) (util.Uint256, uint32, error) { + tx, err := c.RegisterCandidateUnsigned(k) + if err != nil { + return util.Uint256{}, 0, err + } + return c.actor.SignAndSend(tx) +} + +// RegisterCandidateTransaction creates a transaction that adds the given key to +// the list of candidates that can be voted for. The return result from the +// "registerCandidate" method is checked to be true, so transaction fails (with +// FAULT state) if not successful. Notice that for this call to work it must be +// witnessed by the simple account derived from the given key, so use an +// appropriate Actor. The transaction is signed, but not sent to the network, +// instead it's returned to the caller. +// +// Notice that unlike for all other methods the script for this one is not +// test-executed in its final form because most networks have registration price +// set to be much higher than typical RPC server allows to spend during +// test-execution. This adds some risk that it might fail on-chain, but in +// practice it's not likely to happen if signers are set up correctly. +func (c *Contract) RegisterCandidateTransaction(k *keys.PublicKey) (*transaction.Transaction, error) { + tx, err := c.RegisterCandidateUnsigned(k) + if err != nil { + return nil, err + } + err = c.actor.Sign(tx) + if err != nil { + return nil, err + } + return tx, nil +} + +// RegisterCandidateUnsigned creates a transaction that adds the given key to +// the list of candidates that can be voted for. The return result from the +// "registerCandidate" method is checked to be true, so transaction fails (with +// FAULT state) if not successful. Notice that for this call to work it must be +// witnessed by the simple account derived from the given key, so use an +// appropriate Actor. The transaction is not signed and just returned to the +// caller. +// +// Notice that unlike for all other methods the script for this one is not +// test-executed in its final form because most networks have registration price +// set to be much higher than typical RPC server allows to spend during +// test-execution. This adds some risk that it might fail on-chain, but in +// practice it's not likely to happen if signers are set up correctly. +func (c *Contract) RegisterCandidateUnsigned(k *keys.PublicKey) (*transaction.Transaction, error) { + // It's an unregister script intentionally. + r, err := c.actor.Run(regScript(true, k)) + if err != nil { + return nil, err + } + regPrice, err := c.GetRegisterPrice() + if err != nil { + return nil, err + } + return c.actor.MakeUnsignedUncheckedRun(regScript(false, k), r.GasConsumed+regPrice, nil) +} + +// UnregisterCandidate creates and sends a transaction that removes the key from +// the list of candidates that can be voted for. The return result from the +// "unregisterCandidate" method is checked to be true, so transaction fails (with +// FAULT state) if not successful. Notice that for this call to work it must be +// witnessed by the simple account derived from the given key, so use an +// appropriate Actor. The returned values are transaction hash, its +// ValidUntilBlock value and an error if any. +func (c *Contract) UnregisterCandidate(k *keys.PublicKey) (util.Uint256, uint32, error) { + return c.actor.SendRun(regScript(true, k)) +} + +// UnregisterCandidateTransaction creates a transaction that removes the key from +// the list of candidates that can be voted for. The return result from the +// "unregisterCandidate" method is checked to be true, so transaction fails (with +// FAULT state) if not successful. Notice that for this call to work it must be +// witnessed by the simple account derived from the given key, so use an +// appropriate Actor. The transaction is signed, but not sent to the network, +// instead it's returned to the caller. +func (c *Contract) UnregisterCandidateTransaction(k *keys.PublicKey) (*transaction.Transaction, error) { + return c.actor.MakeRun(regScript(true, k)) +} + +// UnregisterCandidateUnsigned creates a transaction that removes the key from +// the list of candidates that can be voted for. The return result from the +// "unregisterCandidate" method is checked to be true, so transaction fails (with +// FAULT state) if not successful. Notice that for this call to work it must be +// witnessed by the simple account derived from the given key, so use an +// appropriate Actor. The transaction is not signed and just returned to the +// caller. +func (c *Contract) UnregisterCandidateUnsigned(k *keys.PublicKey) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedRun(regScript(true, k), nil) +} + +func regScript(unreg bool, k *keys.PublicKey) []byte { + var method = "registerCandidate" + + if unreg { + method = "unregisterCandidate" + } + + // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. + script, _ := smartcontract.CreateCallWithAssertScript(Hash, method, k.Bytes()) + return script +} + +// Vote creates and sends a transaction that casts a vote from the given account +// to the given key which can be nil (in which case any previous vote is removed). +// The return result from the "vote" method is checked to be true, so transaction +// fails (with FAULT state) if voting is not successful. The returned values are +// transaction hash, its ValidUntilBlock value and an error if any. +func (c *Contract) Vote(account util.Uint160, voteTo *keys.PublicKey) (util.Uint256, uint32, error) { + return c.actor.SendRun(voteScript(account, voteTo)) +} + +// VoteTransaction creates a transaction that casts a vote from the given account +// to the given key which can be nil (in which case any previous vote is removed). +// The return result from the "vote" method is checked to be true, so transaction +// fails (with FAULT state) if voting is not successful. The transaction is signed, +// but not sent to the network, instead it's returned to the caller. +func (c *Contract) VoteTransaction(account util.Uint160, voteTo *keys.PublicKey) (*transaction.Transaction, error) { + return c.actor.MakeRun(voteScript(account, voteTo)) +} + +// VoteUnsigned creates a transaction that casts a vote from the given account +// to the given key which can be nil (in which case any previous vote is removed). +// The return result from the "vote" method is checked to be true, so transaction +// fails (with FAULT state) if voting is not successful. The transaction is not +// signed and just returned to the caller. +func (c *Contract) VoteUnsigned(account util.Uint160, voteTo *keys.PublicKey) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedRun(voteScript(account, voteTo), nil) +} + +func voteScript(account util.Uint160, voteTo *keys.PublicKey) []byte { + var param interface{} + + if voteTo != nil { + param = voteTo.Bytes() + } + // We know parameters exactly (unlike with nep17.Transfer), so this can't fail. + script, _ := smartcontract.CreateCallWithAssertScript(Hash, "vote", account, param) + return script +} + +// SetGasPerBlock creates and sends a transaction that sets the new amount of +// GAS to be generated in each block. The action is successful when transaction +// ends in HALT state. Notice that this setting can be changed only by the +// network's committee, so use an appropriate Actor. The returned values are +// transaction hash, its ValidUntilBlock value and an error if any. +func (c *Contract) SetGasPerBlock(gas int64) (util.Uint256, uint32, error) { + return c.actor.SendCall(Hash, setGasMethod, gas) +} + +// SetGasPerBlockTransaction creates a transaction that sets the new amount of +// GAS to be generated in each block. The action is successful when transaction +// ends in HALT state. Notice that this setting can be changed only by the +// network's committee, so use an appropriate Actor. The transaction is signed, +// but not sent to the network, instead it's returned to the caller. +func (c *Contract) SetGasPerBlockTransaction(gas int64) (*transaction.Transaction, error) { + return c.actor.MakeCall(Hash, setGasMethod, gas) +} + +// SetGasPerBlockUnsigned creates a transaction that sets the new amount of +// GAS to be generated in each block. The action is successful when transaction +// ends in HALT state. Notice that this setting can be changed only by the +// network's committee, so use an appropriate Actor. The transaction is not +// signed and just returned to the caller. +func (c *Contract) SetGasPerBlockUnsigned(gas int64) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(Hash, setGasMethod, nil, gas) +} + +// SetRegisterPrice creates and sends a transaction that sets the new candidate +// registration price (in GAS). The action is successful when transaction +// ends in HALT state. Notice that this setting can be changed only by the +// network's committee, so use an appropriate Actor. The returned values are +// transaction hash, its ValidUntilBlock value and an error if any. +func (c *Contract) SetRegisterPrice(price int64) (util.Uint256, uint32, error) { + return c.actor.SendCall(Hash, setRegMethod, price) +} + +// SetRegisterPriceTransaction creates a transaction that sets the new candidate +// registration price (in GAS). The action is successful when transaction +// ends in HALT state. Notice that this setting can be changed only by the +// network's committee, so use an appropriate Actor. The transaction is signed, +// but not sent to the network, instead it's returned to the caller. +func (c *Contract) SetRegisterPriceTransaction(price int64) (*transaction.Transaction, error) { + return c.actor.MakeCall(Hash, setRegMethod, price) +} + +// SetRegisterPriceUnsigned creates a transaction that sets the new candidate +// registration price (in GAS). The action is successful when transaction +// ends in HALT state. Notice that this setting can be changed only by the +// network's committee, so use an appropriate Actor. The transaction is not +// signed and just returned to the caller. +func (c *Contract) SetRegisterPriceUnsigned(price int64) (*transaction.Transaction, error) { + return c.actor.MakeUnsignedCall(Hash, setRegMethod, nil, price) +} diff --git a/pkg/rpcclient/neo/neo_test.go b/pkg/rpcclient/neo/neo_test.go new file mode 100644 index 000000000..5cf7e8235 --- /dev/null +++ b/pkg/rpcclient/neo/neo_test.go @@ -0,0 +1,568 @@ +package neo + +import ( + "errors" + "math/big" + "testing" + + "github.com/google/uuid" + "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/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/stretchr/testify/require" +) + +type testAct struct { + err error + ser error + res *result.Invoke + rre *result.Invoke + rer error + tx *transaction.Transaction + txh util.Uint256 + vub uint32 + inv *result.Invoke +} + +func (t *testAct) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) { + return t.res, t.err +} +func (t *testAct) MakeRun(script []byte) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendRun(script []byte) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} +func (t *testAct) MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} +func (t *testAct) Run(script []byte) (*result.Invoke, error) { + return t.rre, t.rer +} +func (t *testAct) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error) { + return t.tx, t.err +} +func (t *testAct) Sign(tx *transaction.Transaction) error { + return t.ser +} +func (t *testAct) SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error) { + return t.txh, t.vub, t.err +} +func (t *testAct) CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) { + return t.inv, t.err +} +func (t *testAct) TerminateSession(sessionID uuid.UUID) error { + return t.err +} +func (t *testAct) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) { + return t.res.Stack, t.err +} + +func TestGetAccountState(t *testing.T) { + ta := &testAct{} + neo := NewReader(ta) + + ta.err = errors.New("") + _, err := neo.GetAccountState(util.Uint160{}) + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(42), + }, + } + _, err = neo.GetAccountState(util.Uint160{}) + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Null{}, + }, + } + st, err := neo.GetAccountState(util.Uint160{}) + require.NoError(t, err) + require.Nil(t, st) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(100500), + stackitem.Make(42), + stackitem.Null{}, + }), + }, + } + st, err = neo.GetAccountState(util.Uint160{}) + require.NoError(t, err) + require.Equal(t, &state.NEOBalance{ + NEP17Balance: state.NEP17Balance{ + Balance: *big.NewInt(100500), + }, + BalanceHeight: 42, + }, st) +} + +func TestGetAllCandidates(t *testing.T) { + ta := &testAct{} + neo := NewReader(ta) + + ta.err = errors.New("") + _, err := neo.GetAllCandidates() + require.Error(t, err) + + ta.err = nil + iid := uuid.New() + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + ID: &iid, + }), + }, + } + _, err = neo.GetAllCandidates() + require.Error(t, err) + + // Session-based iterator. + sid := uuid.New() + ta.res = &result.Invoke{ + Session: sid, + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + ID: &iid, + }), + }, + } + iter, err := neo.GetAllCandidates() + require.NoError(t, err) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + ta.res = &result.Invoke{ + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(k.PublicKey().Bytes()), + stackitem.Make(100500), + }), + }, + } + vals, err := iter.Next(10) + require.NoError(t, err) + require.Equal(t, 1, len(vals)) + require.Equal(t, result.Validator{ + PublicKey: *k.PublicKey(), + Votes: 100500, + }, vals[0]) + + ta.err = errors.New("") + _, err = iter.Next(1) + require.Error(t, err) + + err = iter.Terminate() + require.Error(t, err) + + // Value-based iterator. + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.NewInterop(result.Iterator{ + Values: []stackitem.Item{ + stackitem.Make(k.PublicKey().Bytes()), + stackitem.Make(100500), + }, + }), + }, + } + iter, err = neo.GetAllCandidates() + require.NoError(t, err) + + ta.err = errors.New("") + err = iter.Terminate() + require.NoError(t, err) +} + +func TestGetCandidates(t *testing.T) { + ta := &testAct{} + neo := NewReader(ta) + + ta.err = errors.New("") + _, err := neo.GetCandidates() + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + }, + } + cands, err := neo.GetCandidates() + require.NoError(t, err) + require.Equal(t, 0, len(cands)) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{stackitem.Make(42)}, + } + _, err = neo.GetCandidates() + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(42), + }), + }, + } + _, err = neo.GetCandidates() + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + }), + }, + } + _, err = neo.GetCandidates() + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Null{}, + stackitem.Null{}, + }), + }), + }, + } + _, err = neo.GetCandidates() + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make("some"), + stackitem.Null{}, + }), + }), + }, + } + _, err = neo.GetCandidates() + require.Error(t, err) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(k.PublicKey().Bytes()), + stackitem.Null{}, + }), + }), + }, + } + _, err = neo.GetCandidates() + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make([]stackitem.Item{ + stackitem.Make(k.PublicKey().Bytes()), + stackitem.Make("canbeabigint"), + }), + }), + }, + } + _, err = neo.GetCandidates() + require.Error(t, err) +} + +func TestGetKeys(t *testing.T) { + ta := &testAct{} + neo := NewReader(ta) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + for _, m := range []func() (keys.PublicKeys, error){neo.GetCommittee, neo.GetNextBlockValidators} { + ta.err = errors.New("") + _, err := m() + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{stackitem.Make(k.PublicKey().Bytes())}), + }, + } + ks, err := m() + require.NoError(t, err) + require.NotNil(t, ks) + require.Equal(t, 1, len(ks)) + require.Equal(t, k.PublicKey(), ks[0]) + } +} + +func TestGetInts(t *testing.T) { + ta := &testAct{} + neo := NewReader(ta) + + meth := []func() (int64, error){ + neo.GetGasPerBlock, + neo.GetRegisterPrice, + } + + ta.err = errors.New("") + for _, m := range meth { + _, err := m() + require.Error(t, err) + } + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(42), + }, + } + for _, m := range meth { + val, err := m() + require.NoError(t, err) + require.Equal(t, int64(42), val) + } +} + +func TestUnclaimedGas(t *testing.T) { + ta := &testAct{} + neo := NewReader(ta) + + ta.err = errors.New("") + _, err := neo.UnclaimedGas(util.Uint160{}, 100500) + require.Error(t, err) + + ta.err = nil + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make([]stackitem.Item{}), + }, + } + _, err = neo.UnclaimedGas(util.Uint160{}, 100500) + require.Error(t, err) + + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(42), + }, + } + val, err := neo.UnclaimedGas(util.Uint160{}, 100500) + require.NoError(t, err) + require.Equal(t, big.NewInt(42), val) +} + +func TestIntSetters(t *testing.T) { + ta := new(testAct) + neo := New(ta) + + meth := []func(int64) (util.Uint256, uint32, error){ + neo.SetGasPerBlock, + neo.SetRegisterPrice, + } + + ta.err = errors.New("") + for _, m := range meth { + _, _, err := m(42) + require.Error(t, err) + } + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + for _, m := range meth { + h, vub, err := m(100) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + } +} + +func TestIntTransactions(t *testing.T) { + ta := new(testAct) + neo := New(ta) + + for _, fun := range []func(int64) (*transaction.Transaction, error){ + neo.SetGasPerBlockTransaction, + neo.SetGasPerBlockUnsigned, + neo.SetRegisterPriceTransaction, + neo.SetRegisterPriceUnsigned, + } { + ta.err = errors.New("") + _, err := fun(1) + require.Error(t, err) + + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := fun(1) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + } +} + +func TestVote(t *testing.T) { + ta := new(testAct) + neo := New(ta) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + + ta.err = errors.New("") + _, _, err = neo.Vote(util.Uint160{}, nil) + require.Error(t, err) + _, _, err = neo.Vote(util.Uint160{}, k.PublicKey()) + require.Error(t, err) + _, err = neo.VoteTransaction(util.Uint160{}, nil) + require.Error(t, err) + _, err = neo.VoteTransaction(util.Uint160{}, k.PublicKey()) + require.Error(t, err) + _, err = neo.VoteUnsigned(util.Uint160{}, nil) + require.Error(t, err) + _, err = neo.VoteUnsigned(util.Uint160{}, k.PublicKey()) + require.Error(t, err) + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + + h, vub, err := neo.Vote(util.Uint160{}, nil) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + h, vub, err = neo.Vote(util.Uint160{}, k.PublicKey()) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := neo.VoteTransaction(util.Uint160{}, nil) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = neo.VoteUnsigned(util.Uint160{}, k.PublicKey()) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) +} + +func TestRegisterCandidate(t *testing.T) { + ta := new(testAct) + neo := New(ta) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + pk := k.PublicKey() + + ta.rer = errors.New("") + _, _, err = neo.RegisterCandidate(pk) + require.Error(t, err) + _, err = neo.RegisterCandidateTransaction(pk) + require.Error(t, err) + _, err = neo.RegisterCandidateUnsigned(pk) + require.Error(t, err) + + ta.rer = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + ta.rre = &result.Invoke{ + GasConsumed: 100500, + } + ta.res = &result.Invoke{ + State: "HALT", + Stack: []stackitem.Item{ + stackitem.Make(42), + }, + } + + h, vub, err := neo.RegisterCandidate(pk) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := neo.RegisterCandidateTransaction(pk) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = neo.RegisterCandidateUnsigned(pk) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + + ta.ser = errors.New("") + _, err = neo.RegisterCandidateTransaction(pk) + require.Error(t, err) + + ta.err = errors.New("") + _, err = neo.RegisterCandidateUnsigned(pk) + require.Error(t, err) +} + +func TestUnregisterCandidate(t *testing.T) { + ta := new(testAct) + neo := New(ta) + + k, err := keys.NewPrivateKey() + require.NoError(t, err) + pk := k.PublicKey() + + ta.err = errors.New("") + _, _, err = neo.UnregisterCandidate(pk) + require.Error(t, err) + _, err = neo.UnregisterCandidateTransaction(pk) + require.Error(t, err) + _, err = neo.UnregisterCandidateUnsigned(pk) + require.Error(t, err) + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + + h, vub, err := neo.UnregisterCandidate(pk) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := neo.UnregisterCandidateTransaction(pk) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) + tx, err = neo.UnregisterCandidateUnsigned(pk) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) +} diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 6d20036cf..feddddb53 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -33,6 +33,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns" "github.com/nspcc-dev/neo-go/pkg/rpcclient/oracle" @@ -291,6 +292,135 @@ func TestClientManagementContract(t *testing.T) { require.Equal(t, 1, len(appLog.Executions[0].Events)) } +func TestClientNEOContract(t *testing.T) { + chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) + defer chain.Close() + defer rpcSrv.Shutdown() + + c, err := rpcclient.New(context.Background(), httpSrv.URL, rpcclient.Options{}) + require.NoError(t, err) + require.NoError(t, c.Init()) + + neoR := neo.NewReader(invoker.New(c, nil)) + + sym, err := neoR.Symbol() + require.NoError(t, err) + require.Equal(t, "NEO", sym) + + dec, err := neoR.Decimals() + require.NoError(t, err) + require.Equal(t, 0, dec) + + ts, err := neoR.TotalSupply() + require.NoError(t, err) + require.Equal(t, big.NewInt(1_0000_0000), ts) + + comm, err := neoR.GetCommittee() + require.NoError(t, err) + commScript, err := smartcontract.CreateMajorityMultiSigRedeemScript(comm) + require.NoError(t, err) + require.Equal(t, testchain.CommitteeScriptHash(), hash.Hash160(commScript)) + + vals, err := neoR.GetNextBlockValidators() + require.NoError(t, err) + valsScript, err := smartcontract.CreateDefaultMultiSigRedeemScript(vals) + require.NoError(t, err) + require.Equal(t, testchain.MultisigScriptHash(), hash.Hash160(valsScript)) + + gpb, err := neoR.GetGasPerBlock() + require.NoError(t, err) + require.Equal(t, int64(5_0000_0000), gpb) + + regP, err := neoR.GetRegisterPrice() + require.NoError(t, err) + require.Equal(t, int64(1000_0000_0000), regP) + + acc0 := testchain.PrivateKey(0).PublicKey().GetScriptHash() + uncl, err := neoR.UnclaimedGas(acc0, 100) + require.NoError(t, err) + require.Equal(t, big.NewInt(48000), uncl) + + accState, err := neoR.GetAccountState(acc0) + require.NoError(t, err) + require.Equal(t, big.NewInt(1000), &accState.Balance) + require.Equal(t, uint32(4), accState.BalanceHeight) + + cands, err := neoR.GetCandidates() + require.NoError(t, err) + require.Equal(t, 0, len(cands)) // No registrations. + + cands, err = neoR.GetAllCandidatesExpanded(100) + require.NoError(t, err) + require.Equal(t, 0, len(cands)) // No registrations. + + iter, err := neoR.GetAllCandidates() + require.NoError(t, err) + cands, err = iter.Next(10) + require.NoError(t, err) + require.Equal(t, 0, len(cands)) // No registrations. + require.NoError(t, iter.Terminate()) + + act, err := actor.New(c, []actor.SignerAccount{{ + Signer: transaction.Signer{ + Account: testchain.CommitteeScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: &wallet.Account{ + Address: testchain.CommitteeAddress(), + Contract: &wallet.Contract{ + Script: testchain.CommitteeVerificationScript(), + }, + }, + }}) + + require.NoError(t, err) + + neoC := neo.New(act) + + txgpb, err := neoC.SetGasPerBlockUnsigned(10 * 1_0000_0000) + require.NoError(t, err) + txregp, err := neoC.SetRegisterPriceUnsigned(1_0000) + require.NoError(t, err) + + for _, tx := range []*transaction.Transaction{txgpb, txregp} { + tx.Scripts[0].InvocationScript = testchain.SignCommittee(tx) + } + + bl := testchain.NewBlock(t, chain, 1, 0, txgpb, txregp) + _, err = c.SubmitBlock(*bl) + require.NoError(t, err) + + gpb, err = neoR.GetGasPerBlock() + require.NoError(t, err) + require.Equal(t, int64(10_0000_0000), gpb) + + regP, err = neoR.GetRegisterPrice() + require.NoError(t, err) + require.Equal(t, int64(10000), regP) + + act0, err := actor.NewSimple(c, wallet.NewAccountFromPrivateKey(testchain.PrivateKey(0))) + require.NoError(t, err) + neo0 := neo.New(act0) + + txreg, err := neo0.RegisterCandidateTransaction(testchain.PrivateKey(0).PublicKey()) + require.NoError(t, err) + bl = testchain.NewBlock(t, chain, 1, 0, txreg) + _, err = c.SubmitBlock(*bl) + require.NoError(t, err) + + txvote, err := neo0.VoteTransaction(acc0, testchain.PrivateKey(0).PublicKey()) + require.NoError(t, err) + bl = testchain.NewBlock(t, chain, 1, 0, txvote) + _, err = c.SubmitBlock(*bl) + require.NoError(t, err) + + txunreg, err := neo0.UnregisterCandidateTransaction(testchain.PrivateKey(0).PublicKey()) + require.NoError(t, err) + bl = testchain.NewBlock(t, chain, 1, 0, txunreg) + _, err = c.SubmitBlock(*bl) + require.NoError(t, err) +} + func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { chain, rpcSrv, httpSrv := initServerWithInMemoryChain(t) defer chain.Close()