neo-go/pkg/rpcclient/neo/neo.go
2024-05-14 09:39:53 +03:00

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/nativehashes"
"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 = nativehashes.NeoToken
// 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)
}