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
|
||||
// is the length of blockchain validators list got from GetNextBlockValidators()
|
||||
// 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) {
|
||||
var (
|
||||
result uint32
|
||||
|
|
Loading…
Reference in a new issue