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()