neoneo-go/pkg/rpcclient/actor/actor.go
Roman Khimov c0705e45c9 rpcclient: add actor package
Somewhat similar to invoker, but changing the state (or just creating a
transaction). Transaction creation could've been put into a structure of its
own, but it seems to be less convenient to use this way.
2022-08-07 22:33:56 +03:00

204 lines
8.3 KiB
Go

/*
Package actor provides a way to change chain state via RPC client.
This layer builds on top of the basic RPC client and simplifies creating,
signing and sending transactions to the network (since that's the only way chain
state is changed). It's generic enough to be used for any contract that you may
want to invoke and contract-specific functions can build on top of it.
*/
package actor
import (
"errors"
"fmt"
"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/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
// RPCActor is an interface required from the RPC client to successfully
// create and send transactions.
type RPCActor interface {
invoker.RPCInvoke
CalculateNetworkFee(tx *transaction.Transaction) (int64, error)
GetBlockCount() (uint32, error)
GetVersion() (*result.Version, error)
SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error)
}
// SignerAccount represents combination of the transaction.Signer and the
// corresponding wallet.Account. It's used to create and sign transactions, each
// transaction has a set of signers that must witness the transaction with their
// signatures.
type SignerAccount struct {
Signer transaction.Signer
Account *wallet.Account
}
// Actor keeps a connection to the RPC endpoint and allows to perform
// state-changing actions (via transactions that can also be created without
// sending them to the network) on behalf of a set of signers. It also provides
// an Invoker interface to perform test calls with the same set of signers.
type Actor struct {
invoker.Invoker
client RPCActor
signers []SignerAccount
txSigners []transaction.Signer
version *result.Version
}
// New creates an Actor instance using the specified RPC interface and the set of
// signers with corresponding accounts. Every transaction created by this Actor
// will have this set of signers and all communication will be performed via this
// RPC. Upon Actor instance creation a GetVersion call is made and the result of
// it is cached forever (and used for internal purposes).
func New(ra RPCActor, signers []SignerAccount) (*Actor, error) {
if len(signers) < 1 {
return nil, errors.New("at least one signer (sender) is required")
}
invSigners := make([]transaction.Signer, len(signers))
for i := range signers {
if signers[i].Account.Contract == nil {
return nil, fmt.Errorf("empty contract for account %s", signers[i].Account.Address)
}
if !signers[i].Account.Contract.Deployed && signers[i].Account.Contract.ScriptHash() != signers[i].Signer.Account {
return nil, fmt.Errorf("signer account doesn't match script hash for signer %s", signers[i].Account.Address)
}
invSigners[i] = signers[i].Signer
}
inv := invoker.New(ra, invSigners)
version, err := ra.GetVersion()
if err != nil {
return nil, err
}
return &Actor{
Invoker: *inv,
client: ra,
signers: signers,
txSigners: invSigners,
version: version,
}, nil
}
// NewSimple makes it easier to create an Actor for the most widespread case
// when transactions have only one signer that uses CalledByEntry scope. When
// other scopes or multiple signers are needed use New.
func NewSimple(ra RPCActor, acc *wallet.Account) (*Actor, error) {
return New(ra, []SignerAccount{{
Signer: transaction.Signer{
Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry,
},
Account: acc,
}})
}
// CalculateNetworkFee wraps RPCActor's CalculateNetworkFee, making it available
// to Actor users directly. It returns network fee value for the given
// transaction.
func (a *Actor) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
return a.client.CalculateNetworkFee(tx)
}
// GetBlockCount wraps RPCActor's GetBlockCount, making it available to
// Actor users directly. It returns current number of blocks in the chain.
func (a *Actor) GetBlockCount() (uint32, error) {
return a.client.GetBlockCount()
}
// GetNetwork is a convenience method that returns the network's magic number.
func (a *Actor) GetNetwork() netmode.Magic {
return a.version.Protocol.Network
}
// GetVersion returns version data from the RPC endpoint.
func (a *Actor) GetVersion() result.Version {
return *a.version
}
// Send allows to send arbitrary prepared transaction to the network. It returns
// transaction hash and ValidUntilBlock value.
func (a *Actor) Send(tx *transaction.Transaction) (util.Uint256, uint32, error) {
h, err := a.client.SendRawTransaction(tx)
return h, tx.ValidUntilBlock, err
}
// Sign adds signatures to arbitrary transaction using Actor signers wallets.
// Most of the time it shouldn't be used directly since it'll be successful only
// if the transaction is made using the same set of accounts as the one used
// for Actor creation.
func (a *Actor) Sign(tx *transaction.Transaction) error {
if len(tx.Signers) != len(a.signers) {
return errors.New("incorrect number of signers in the transaction")
}
for i, signer := range a.signers {
err := signer.Account.SignTx(a.GetNetwork(), tx)
if err != nil { // then account is non-contract-based and locked, but let's provide more detailed error
if paramNum := len(signer.Account.Contract.Parameters); paramNum != 0 && signer.Account.Contract.Deployed {
return fmt.Errorf("failed to add contract-based witness for signer #%d (%s): "+
"%d parameters must be provided to construct invocation script", i, signer.Account.Address, paramNum)
}
return fmt.Errorf("failed to add witness for signer #%d (%s): account should be unlocked to add the signature. "+
"Store partially-signed transaction and then use 'wallet sign' command to cosign it", i, signer.Account.Address)
}
}
return nil
}
// SignAndSend signs arbitrary transaction (see also Sign) and sends it to the
// network.
func (a *Actor) SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error) {
return a.sendWrapper(tx, a.Sign(tx))
}
// sendWrapper simplifies wrapping methods that create transactions.
func (a *Actor) sendWrapper(tx *transaction.Transaction, err error) (util.Uint256, uint32, error) {
if err != nil {
return util.Uint256{}, 0, err
}
return a.Send(tx)
}
// SendCall creates a transaction that calls the given method of the given
// contract with the given parameters (see also MakeCall) and sends it to the
// network.
func (a *Actor) SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) {
return a.sendWrapper(a.MakeCall(contract, method, params...))
}
// SendTunedCall creates a transaction that calls the given method of the given
// contract with the given parameters (see also MakeTunedCall) and attributes,
// allowing to check for execution results of this call and modify transaction
// before it's signed; this transaction is then sent to the network.
func (a *Actor) SendTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...interface{}) (util.Uint256, uint32, error) {
return a.sendWrapper(a.MakeTunedCall(contract, method, attrs, txHook, params...))
}
// SendRun creates a transaction with the given executable script (see also
// MakeRun) and sends it to the network.
func (a *Actor) SendRun(script []byte) (util.Uint256, uint32, error) {
return a.sendWrapper(a.MakeRun(script))
}
// SendTunedRun creates a transaction with the given executable script and
// attributes, allowing to check for execution results of this script and modify
// transaction before it's signed (see also MakeTunedRun). This transaction is
// then sent to the network.
func (a *Actor) SendTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (util.Uint256, uint32, error) {
return a.sendWrapper(a.MakeTunedRun(script, attrs, txHook))
}
// SendUncheckedRun creates a transaction with the given executable script and
// attributes that can use up to sysfee GAS for its execution, allowing to modify
// this transaction before it's signed (see also MakeUncheckedRun). This
// transaction is then sent to the network.
func (a *Actor) SendUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (util.Uint256, uint32, error) {
return a.sendWrapper(a.MakeUncheckedRun(script, sysfee, attrs, txHook))
}