e0328cc1f8
Port neo-project/neo#3154. Close #3334 Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
474 lines
20 KiB
Go
474 lines
20 KiB
Go
/*
|
|
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 ...any) (*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 ...any) (*transaction.Transaction, error)
|
|
MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error)
|
|
MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error)
|
|
SendCall(contract util.Uint160, method string, params ...any) (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.TokenWriter
|
|
|
|
actor Actor
|
|
}
|
|
|
|
// CandidateStateEvent represents a CandidateStateChanged NEO event.
|
|
type CandidateStateEvent struct {
|
|
Key *keys.PublicKey
|
|
Registered bool
|
|
Votes *big.Int
|
|
}
|
|
|
|
// CommitteeChangedEvent represents a CommitteeChanged NEO event.
|
|
type CommitteeChangedEvent struct {
|
|
Old []keys.PublicKey
|
|
New []keys.PublicKey
|
|
}
|
|
|
|
// 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 {
|
|
nep := nep17.New(actor, Hash)
|
|
return &Contract{ContractReader{nep.TokenReader, actor}, nep.TokenWriter, 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"))
|
|
}
|
|
|
|
// GetCommitteeAddress returns the committee address.
|
|
func (c *ContractReader) GetCommitteeAddress() (util.Uint160, error) {
|
|
return unwrap.Uint160(c.invoker.Call(Hash, "getCommitteeAddress"))
|
|
}
|
|
|
|
// 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 any
|
|
|
|
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)
|
|
}
|