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.
This commit is contained in:
parent
d4292ed532
commit
c0705e45c9
5 changed files with 817 additions and 0 deletions
204
pkg/rpcclient/actor/actor.go
Normal file
204
pkg/rpcclient/actor/actor.go
Normal file
|
@ -0,0 +1,204 @@
|
||||||
|
/*
|
||||||
|
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))
|
||||||
|
}
|
254
pkg/rpcclient/actor/actor_test.go
Normal file
254
pkg/rpcclient/actor/actor_test.go
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
package actor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/wallet"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RPCClient struct {
|
||||||
|
err error
|
||||||
|
invRes *result.Invoke
|
||||||
|
netFee int64
|
||||||
|
bCount uint32
|
||||||
|
version *result.Version
|
||||||
|
hash util.Uint256
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RPCClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
|
||||||
|
return r.invRes, r.err
|
||||||
|
}
|
||||||
|
func (r *RPCClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
|
||||||
|
return r.invRes, r.err
|
||||||
|
}
|
||||||
|
func (r *RPCClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
|
||||||
|
return r.invRes, r.err
|
||||||
|
}
|
||||||
|
func (r *RPCClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
|
||||||
|
return r.netFee, r.err
|
||||||
|
}
|
||||||
|
func (r *RPCClient) GetBlockCount() (uint32, error) {
|
||||||
|
return r.bCount, r.err
|
||||||
|
}
|
||||||
|
func (r *RPCClient) GetVersion() (*result.Version, error) {
|
||||||
|
verCopy := *r.version
|
||||||
|
return &verCopy, r.err
|
||||||
|
}
|
||||||
|
func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) {
|
||||||
|
return r.hash, r.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func testRPCAndAccount(t *testing.T) (*RPCClient, *wallet.Account) {
|
||||||
|
client := &RPCClient{
|
||||||
|
version: &result.Version{
|
||||||
|
Protocol: result.Protocol{
|
||||||
|
Network: netmode.UnitTestNet,
|
||||||
|
MillisecondsPerBlock: 1000,
|
||||||
|
ValidatorsCount: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
acc, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
return client, acc
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
client, acc := testRPCAndAccount(t)
|
||||||
|
|
||||||
|
// No signers.
|
||||||
|
_, err := New(client, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
_, err = New(client, []SignerAccount{})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good simple.
|
||||||
|
a, err := NewSimple(client, acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 1, len(a.signers))
|
||||||
|
require.Equal(t, 1, len(a.txSigners))
|
||||||
|
require.Equal(t, transaction.CalledByEntry, a.signers[0].Signer.Scopes)
|
||||||
|
require.Equal(t, transaction.CalledByEntry, a.txSigners[0].Scopes)
|
||||||
|
|
||||||
|
// Contractless account.
|
||||||
|
badAcc, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
badAccHash := badAcc.Contract.ScriptHash()
|
||||||
|
badAcc.Contract = nil
|
||||||
|
|
||||||
|
signers := []SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: acc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
Account: acc,
|
||||||
|
}, {
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: badAccHash,
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
},
|
||||||
|
Account: badAcc,
|
||||||
|
}}
|
||||||
|
|
||||||
|
_, err = New(client, signers)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// GetVersion returning error.
|
||||||
|
client.err = errors.New("bad")
|
||||||
|
_, err = NewSimple(client, acc)
|
||||||
|
require.Error(t, err)
|
||||||
|
client.err = nil
|
||||||
|
|
||||||
|
// Account mismatch.
|
||||||
|
acc2, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
signers = []SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: acc2.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
Account: acc,
|
||||||
|
}, {
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: acc2.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
},
|
||||||
|
Account: acc2,
|
||||||
|
}}
|
||||||
|
_, err = New(client, signers)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good multiaccount.
|
||||||
|
signers[0].Signer.Account = acc.Contract.ScriptHash()
|
||||||
|
a, err = New(client, signers)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(a.signers))
|
||||||
|
require.Equal(t, 2, len(a.txSigners))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSimpleWrappers(t *testing.T) {
|
||||||
|
client, acc := testRPCAndAccount(t)
|
||||||
|
origVer := *client.version
|
||||||
|
|
||||||
|
a, err := NewSimple(client, acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.netFee = 42
|
||||||
|
nf, err := a.CalculateNetworkFee(new(transaction.Transaction))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(42), nf)
|
||||||
|
|
||||||
|
client.bCount = 100500
|
||||||
|
bc, err := a.GetBlockCount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(100500), bc)
|
||||||
|
|
||||||
|
require.Equal(t, netmode.UnitTestNet, a.GetNetwork())
|
||||||
|
client.version.Protocol.Network = netmode.TestNet
|
||||||
|
require.Equal(t, netmode.UnitTestNet, a.GetNetwork())
|
||||||
|
require.Equal(t, origVer, a.GetVersion())
|
||||||
|
|
||||||
|
a, err = NewSimple(client, acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, netmode.TestNet, a.GetNetwork())
|
||||||
|
require.Equal(t, *client.version, a.GetVersion())
|
||||||
|
|
||||||
|
client.hash = util.Uint256{1, 2, 3}
|
||||||
|
h, vub, err := a.Send(&transaction.Transaction{ValidUntilBlock: 123})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, client.hash, h)
|
||||||
|
require.Equal(t, uint32(123), vub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSign(t *testing.T) {
|
||||||
|
client, acc := testRPCAndAccount(t)
|
||||||
|
acc2, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
a, err := New(client, []SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: acc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.None,
|
||||||
|
},
|
||||||
|
Account: acc,
|
||||||
|
}, {
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: acc2.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
},
|
||||||
|
Account: &wallet.Account{ // Looks like acc2, but has no private key.
|
||||||
|
Address: acc2.Address,
|
||||||
|
EncryptedWIF: acc2.EncryptedWIF,
|
||||||
|
Contract: acc2.Contract,
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
script := []byte{1, 2, 3}
|
||||||
|
client.hash = util.Uint256{2, 5, 6}
|
||||||
|
client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
|
||||||
|
|
||||||
|
tx, err := a.MakeUnsignedRun(script, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Error(t, a.Sign(tx))
|
||||||
|
_, _, err = a.SignAndSend(tx)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSenders(t *testing.T) {
|
||||||
|
client, acc := testRPCAndAccount(t)
|
||||||
|
a, err := NewSimple(client, acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
script := []byte{1, 2, 3}
|
||||||
|
|
||||||
|
// Bad.
|
||||||
|
client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script}
|
||||||
|
_, _, err = a.SendCall(util.Uint160{1}, "method", 42)
|
||||||
|
require.Error(t, err)
|
||||||
|
_, _, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42)
|
||||||
|
require.Error(t, err)
|
||||||
|
_, _, err = a.SendRun(script)
|
||||||
|
require.Error(t, err)
|
||||||
|
_, _, err = a.SendTunedRun(script, nil, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
_, _, err = a.SendUncheckedRun(script, 1, nil, func(t *transaction.Transaction) error {
|
||||||
|
return errors.New("bad")
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good.
|
||||||
|
client.hash = util.Uint256{2, 5, 6}
|
||||||
|
client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
|
||||||
|
h, vub, err := a.SendCall(util.Uint160{1}, "method", 42)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, client.hash, h)
|
||||||
|
require.Equal(t, uint32(8), vub)
|
||||||
|
|
||||||
|
h, vub, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, client.hash, h)
|
||||||
|
require.Equal(t, uint32(8), vub)
|
||||||
|
|
||||||
|
h, vub, err = a.SendRun(script)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, client.hash, h)
|
||||||
|
require.Equal(t, uint32(8), vub)
|
||||||
|
|
||||||
|
h, vub, err = a.SendTunedRun(script, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, client.hash, h)
|
||||||
|
require.Equal(t, uint32(8), vub)
|
||||||
|
|
||||||
|
h, vub, err = a.SendUncheckedRun(script, 1, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, client.hash, h)
|
||||||
|
require.Equal(t, uint32(8), vub)
|
||||||
|
}
|
195
pkg/rpcclient/actor/maker.go
Normal file
195
pkg/rpcclient/actor/maker.go
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
package actor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransactionCheckerModifier is a callback that receives the result of
|
||||||
|
// test-invocation and the transaction that can perform the same invocation
|
||||||
|
// on chain. This callback is accepted by methods that create transactions, it
|
||||||
|
// can examine both arguments and return an error if there is anything wrong
|
||||||
|
// there which will abort the creation process. Notice that when used this
|
||||||
|
// callback is completely responsible for invocation result checking, including
|
||||||
|
// checking for HALT execution state (so if you don't check for it in a callback
|
||||||
|
// you can send a transaction that is known to end up in FAULT state). It can
|
||||||
|
// also modify the transaction (see TransactionModifier).
|
||||||
|
type TransactionCheckerModifier func(r *result.Invoke, t *transaction.Transaction) error
|
||||||
|
|
||||||
|
// TransactionModifier is a callback that receives the transaction before
|
||||||
|
// it's signed from a method that creates signed transactions. It can check
|
||||||
|
// fees and other fields of the transaction and return an error if there is
|
||||||
|
// anything wrong there which will abort the creation process. It also can modify
|
||||||
|
// Nonce, SystemFee, NetworkFee and ValidUntilBlock values taking full
|
||||||
|
// responsibility on the effects of these modifications (smaller fee values, too
|
||||||
|
// low or too high ValidUntilBlock or bad Nonce can render transaction invalid).
|
||||||
|
// Modifying other fields is not supported. Mostly it's useful for increasing
|
||||||
|
// fee values since by default they're just enough for transaction to be
|
||||||
|
// successfully accepted and executed.
|
||||||
|
type TransactionModifier func(t *transaction.Transaction) error
|
||||||
|
|
||||||
|
// MakeCall creates a transaction that calls the given method of the given
|
||||||
|
// contract with the given parameters. Test call is performed and checked for
|
||||||
|
// HALT status, if more checks are needed or transaction should have some
|
||||||
|
// additional attributes use MakeTunedCall.
|
||||||
|
func (a *Actor) MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) {
|
||||||
|
return a.MakeTunedCall(contract, method, nil, nil, params...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeTunedCall creates a transaction with the given attributes that calls the
|
||||||
|
// given method of the given contract with the given parameters. It's filtered
|
||||||
|
// through the provided callback (see TransactionCheckerModifier documentation),
|
||||||
|
// so the process can be aborted and transaction can be modified before signing.
|
||||||
|
// If no callback is given then the result is checked for HALT state.
|
||||||
|
func (a *Actor) MakeTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...interface{}) (*transaction.Transaction, error) {
|
||||||
|
r, err := a.Call(contract, method, params...)
|
||||||
|
return a.makeUncheckedWrapper(r, err, attrs, txHook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeRun creates a transaction with the given executable script. Test
|
||||||
|
// invocation of this script is performed and expected to end up in HALT
|
||||||
|
// state. If more checks are needed or transaction should have some additional
|
||||||
|
// attributes use MakeTunedRun.
|
||||||
|
func (a *Actor) MakeRun(script []byte) (*transaction.Transaction, error) {
|
||||||
|
return a.MakeTunedRun(script, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeTunedRun creates a transaction with the given attributes that executes
|
||||||
|
// the given script. It's filtered through the provided callback (see
|
||||||
|
// TransactionCheckerModifier documentation), so the process can be aborted and
|
||||||
|
// transaction can be modified before signing. If no callback is given then the
|
||||||
|
// result is checked for HALT state.
|
||||||
|
func (a *Actor) MakeTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) {
|
||||||
|
r, err := a.Run(script)
|
||||||
|
return a.makeUncheckedWrapper(r, err, attrs, txHook)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Actor) makeUncheckedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("test invocation failed: %w", err)
|
||||||
|
}
|
||||||
|
return a.MakeUncheckedRun(r.Script, r.GasConsumed, attrs, func(tx *transaction.Transaction) error {
|
||||||
|
if txHook == nil {
|
||||||
|
if r.State != vmstate.Halt.String() {
|
||||||
|
return fmt.Errorf("script failed (%s state) due to an error: %s", r.State, r.FaultException)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return txHook(r, tx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeUncheckedRun creates a transaction with the given attributes that executes
|
||||||
|
// the given script and is expected to use up to sysfee GAS for its execution.
|
||||||
|
// The transaction is filtered through the provided callback (see
|
||||||
|
// TransactionModifier documentation), so the process can be aborted and
|
||||||
|
// transaction can be modified before signing. This method is mostly useful when
|
||||||
|
// test invocation is already performed and the script and required system fee
|
||||||
|
// values are already known.
|
||||||
|
func (a *Actor) MakeUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (*transaction.Transaction, error) {
|
||||||
|
tx, err := a.MakeUnsignedUncheckedRun(script, sysfee, attrs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if txHook != nil {
|
||||||
|
err = txHook(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = a.Sign(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeUnsignedCall creates an unsigned transaction with the given attributes
|
||||||
|
// that calls the given method of the given contract with the given parameters.
|
||||||
|
// Test-invocation is performed and is expected to end up in HALT state, the
|
||||||
|
// transaction returned has correct SystemFee and NetworkFee values.
|
||||||
|
func (a *Actor) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) {
|
||||||
|
r, err := a.Call(contract, method, params...)
|
||||||
|
return a.makeUnsignedWrapper(r, err, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeUnsignedRun creates an unsigned transaction with the given attributes
|
||||||
|
// that executes the given script. Test-invocation is performed and is expected
|
||||||
|
// to end up in HALT state, the transaction returned has correct SystemFee and
|
||||||
|
// NetworkFee values.
|
||||||
|
func (a *Actor) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) {
|
||||||
|
r, err := a.Run(script)
|
||||||
|
return a.makeUnsignedWrapper(r, err, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Actor) makeUnsignedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute) (*transaction.Transaction, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to test-invoke: %w", err)
|
||||||
|
}
|
||||||
|
if r.State != vmstate.Halt.String() {
|
||||||
|
return nil, fmt.Errorf("test invocation faulted (%s): %s", r.State, r.FaultException)
|
||||||
|
}
|
||||||
|
return a.MakeUnsignedUncheckedRun(r.Script, r.GasConsumed, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeUnsignedUncheckedRun creates an unsigned transaction containing the given
|
||||||
|
// script with the system fee value and attributes. It's expected to be used when
|
||||||
|
// test invocation is already done and the script and system fee value are already
|
||||||
|
// known to be good, so it doesn't do test invocation internally. But it fills
|
||||||
|
// Signers with Actor's signers, calculates proper ValidUntilBlock and NetworkFee
|
||||||
|
// values. The resulting transaction can be changed in its Nonce, SystemFee,
|
||||||
|
// NetworkFee and ValidUntilBlock values and then be signed and sent or
|
||||||
|
// exchanged via context.ParameterContext.
|
||||||
|
func (a *Actor) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(script) == 0 {
|
||||||
|
return nil, errors.New("empty script")
|
||||||
|
}
|
||||||
|
if sysFee < 0 {
|
||||||
|
return nil, errors.New("negative system fee")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := transaction.New(script, sysFee)
|
||||||
|
tx.Signers = a.txSigners
|
||||||
|
tx.Attributes = attrs
|
||||||
|
|
||||||
|
tx.ValidUntilBlock, err = a.CalculateValidUntilBlock()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("calculating validUntilBlock: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Scripts = make([]transaction.Witness, len(a.signers))
|
||||||
|
for i := range a.signers {
|
||||||
|
if !a.signers[i].Account.Contract.Deployed {
|
||||||
|
tx.Scripts[i].VerificationScript = a.signers[i].Account.Contract.Script
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// CalculateNetworkFee doesn't call Hash or Size, only serializes the
|
||||||
|
// transaction via Bytes, so it's safe wrt internal caching.
|
||||||
|
tx.NetworkFee, err = a.client.CalculateNetworkFee(tx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("calculating network fee: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateValidUntilBlock returns correct ValidUntilBlock value for a new
|
||||||
|
// transaction relative to the current blockchain height. It uses "height +
|
||||||
|
// number of validators + 1" formula suggesting shorter transaction lifetime
|
||||||
|
// than the usual "height + MaxValidUntilBlockIncrement" approach. Shorter
|
||||||
|
// lifetime can be useful to control transaction acceptance wait time because
|
||||||
|
// it can't be added into a block after ValidUntilBlock.
|
||||||
|
func (a *Actor) CalculateValidUntilBlock() (uint32, error) {
|
||||||
|
blockCount, err := a.client.GetBlockCount()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("can't get block count: %w", err)
|
||||||
|
}
|
||||||
|
return blockCount + uint32(a.version.Protocol.ValidatorsCount) + 1, nil
|
||||||
|
}
|
161
pkg/rpcclient/actor/maker_test.go
Normal file
161
pkg/rpcclient/actor/maker_test.go
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
package actor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/util"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCalculateValidUntilBlock(t *testing.T) {
|
||||||
|
client, acc := testRPCAndAccount(t)
|
||||||
|
a, err := NewSimple(client, acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client.err = errors.New("error")
|
||||||
|
_, err = a.CalculateValidUntilBlock()
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
client.err = nil
|
||||||
|
client.bCount = 42
|
||||||
|
vub, err := a.CalculateValidUntilBlock()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(42+7+1), vub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeUnsigned(t *testing.T) {
|
||||||
|
client, acc := testRPCAndAccount(t)
|
||||||
|
a, err := NewSimple(client, acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Bad parameters.
|
||||||
|
script := []byte{1, 2, 3}
|
||||||
|
_, err = a.MakeUnsignedUncheckedRun(script, -1, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
_, err = a.MakeUnsignedUncheckedRun([]byte{}, 1, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
_, err = a.MakeUnsignedUncheckedRun(nil, 1, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// RPC error.
|
||||||
|
client.err = errors.New("err")
|
||||||
|
_, err = a.MakeUnsignedUncheckedRun(script, 1, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good unchecked.
|
||||||
|
client.netFee = 42
|
||||||
|
client.bCount = 100500
|
||||||
|
client.err = nil
|
||||||
|
tx, err := a.MakeUnsignedUncheckedRun(script, 1, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, script, tx.Script)
|
||||||
|
require.Equal(t, 1, len(tx.Signers))
|
||||||
|
require.Equal(t, acc.Contract.ScriptHash(), tx.Signers[0].Account)
|
||||||
|
require.Equal(t, 1, len(tx.Scripts))
|
||||||
|
require.Equal(t, acc.Contract.Script, tx.Scripts[0].VerificationScript)
|
||||||
|
require.Nil(t, tx.Scripts[0].InvocationScript)
|
||||||
|
|
||||||
|
// Bad run.
|
||||||
|
client.err = errors.New("")
|
||||||
|
_, err = a.MakeUnsignedRun(script, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Faulted run.
|
||||||
|
client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script}
|
||||||
|
client.err = nil
|
||||||
|
_, err = a.MakeUnsignedRun(script, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good run.
|
||||||
|
client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
|
||||||
|
_, err = a.MakeUnsignedRun(script, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMakeSigned(t *testing.T) {
|
||||||
|
client, acc := testRPCAndAccount(t)
|
||||||
|
a, err := NewSimple(client, acc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Bad script.
|
||||||
|
_, err = a.MakeUncheckedRun(nil, 0, nil, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good, no hook.
|
||||||
|
script := []byte{1, 2, 3}
|
||||||
|
_, err = a.MakeUncheckedRun(script, 0, nil, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Bad, can't sign because of a hook.
|
||||||
|
_, err = a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error {
|
||||||
|
t.Signers = append(t.Signers, transaction.Signer{})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Bad, hook returns an error.
|
||||||
|
_, err = a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error {
|
||||||
|
return errors.New("")
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good with a hook.
|
||||||
|
tx, err := a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error {
|
||||||
|
t.ValidUntilBlock = 777
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, uint32(777), tx.ValidUntilBlock)
|
||||||
|
|
||||||
|
// Checked
|
||||||
|
|
||||||
|
// Bad, invocation fails.
|
||||||
|
client.err = errors.New("")
|
||||||
|
_, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Bad, hook returns an error.
|
||||||
|
client.err = nil
|
||||||
|
client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
|
||||||
|
_, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error {
|
||||||
|
return errors.New("")
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good, no hook.
|
||||||
|
_, err = a.MakeTunedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}}, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = a.MakeRun(script)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Bad, invocation returns FAULT.
|
||||||
|
client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script}
|
||||||
|
_, err = a.MakeTunedRun(script, nil, nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good, invocation returns FAULT, but callback ignores it.
|
||||||
|
_, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Good, via call and with a callback.
|
||||||
|
_, err = a.MakeTunedCall(util.Uint160{}, "something", []transaction.Attribute{{Type: transaction.HighPriority}}, func(r *result.Invoke, t *transaction.Transaction) error {
|
||||||
|
return nil
|
||||||
|
}, "param", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Bad, it still is a FAULT.
|
||||||
|
_, err = a.MakeCall(util.Uint160{}, "method")
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// Good.
|
||||||
|
client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
|
||||||
|
_, err = a.MakeCall(util.Uint160{}, "method", 1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
|
@ -1006,6 +1006,9 @@ func (c *Client) ValidateAddress(address string) error {
|
||||||
// current blockchain height + number of validators. Number of validators
|
// current blockchain height + number of validators. Number of validators
|
||||||
// is the length of blockchain validators list got from GetNextBlockValidators()
|
// is the length of blockchain validators list got from GetNextBlockValidators()
|
||||||
// method. Validators count is being cached and updated every 100 blocks.
|
// method. Validators count is being cached and updated every 100 blocks.
|
||||||
|
//
|
||||||
|
// Deprecated: please use (*Actor).CalculateValidUntilBlock. This method will be
|
||||||
|
// removed in future versions.
|
||||||
func (c *Client) CalculateValidUntilBlock() (uint32, error) {
|
func (c *Client) CalculateValidUntilBlock() (uint32, error) {
|
||||||
var (
|
var (
|
||||||
result uint32
|
result uint32
|
||||||
|
|
Loading…
Reference in a new issue