Merge pull request #2650 from nspcc-dev/neo-wrapper

NEO RPC wrapper
This commit is contained in:
Roman Khimov 2022-08-19 10:34:41 +03:00 committed by GitHub
commit 8cd4948e06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1394 additions and 178 deletions

View file

@ -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 {

View file

@ -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]

View file

@ -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.

View file

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

View file

@ -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{

View file

@ -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
}

View file

@ -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)
}
})
}

View file

@ -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")
}

462
pkg/rpcclient/neo/neo.go Normal file
View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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"
@ -153,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
}
@ -190,6 +195,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) {

View file

@ -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)
},
@ -193,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)
@ -224,6 +231,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)

View file

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