actor: allow providing default attributes/hooks to be used

Which expands Actor use cases greatly.
This commit is contained in:
Roman Khimov 2022-08-25 16:40:32 +03:00
parent fe50879bb7
commit a95984febf
4 changed files with 158 additions and 37 deletions

View file

@ -48,16 +48,35 @@ type Actor struct {
invoker.Invoker
client RPCActor
opts Options
signers []SignerAccount
txSigners []transaction.Signer
version *result.Version
}
// Options are used to create Actor with non-standard transaction checkers or
// additional attributes to be applied for all transactions.
type Options struct {
// Attributes are set as is into every transaction created by Actor,
// unless they're explicitly set in a method call that accepts
// attributes (like MakeTuned* or MakeUnsigned*).
Attributes []transaction.Attribute
// CheckerModifier is used by any method that creates and signs a
// transaction inside (some of them provide ways to override this
// default, some don't).
CheckerModifier TransactionCheckerModifier
// Modifier is used only by MakeUncheckedRun to modify transaction
// before it's signed (other methods that perform test invocations
// use CheckerModifier). MakeUnsigned* methods do not run it.
Modifier TransactionModifier
}
// 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).
// it is cached forever (and used for internal purposes). The actor will use
// default Options (which can be overridden using NewTuned).
func New(ra RPCActor, signers []SignerAccount) (*Actor, error) {
if len(signers) < 1 {
return nil, errors.New("at least one signer (sender) is required")
@ -81,6 +100,7 @@ func New(ra RPCActor, signers []SignerAccount) (*Actor, error) {
return &Actor{
Invoker: *inv,
client: ra,
opts: NewDefaultOptions(),
signers: signers,
txSigners: invSigners,
version: version,
@ -100,6 +120,34 @@ func NewSimple(ra RPCActor, acc *wallet.Account) (*Actor, error) {
}})
}
// NewDefaultOptions returns Options that have no attributes and use the default
// TransactionCheckerModifier function (that checks for the invocation result to
// be in HALT state) and TransactionModifier (that does nothing).
func NewDefaultOptions() Options {
return Options{
CheckerModifier: DefaultCheckerModifier,
Modifier: DefaultModifier,
}
}
// NewTuned creates an Actor that will use the specified Options as defaults when
// creating new transactions. If checker/modifier callbacks are not provided
// (nil), then default ones (from NewDefaultOptions) are used.
func NewTuned(ra RPCActor, signers []SignerAccount, opts Options) (*Actor, error) {
a, err := New(ra, signers)
if err != nil {
return nil, err
}
a.opts.Attributes = opts.Attributes
if opts.CheckerModifier != nil {
a.opts.CheckerModifier = opts.CheckerModifier
}
if opts.Modifier != nil {
a.opts.Modifier = opts.Modifier
}
return a, err
}
// CalculateNetworkFee wraps RPCActor's CalculateNetworkFee, making it available
// to Actor users directly. It returns network fee value for the given
// transaction.

View file

@ -78,6 +78,9 @@ func TestNew(t *testing.T) {
_, err = New(client, []SignerAccount{})
require.Error(t, err)
_, err = NewTuned(client, []SignerAccount{}, NewDefaultOptions())
require.Error(t, err)
// Good simple.
a, err := NewSimple(client, acc)
require.NoError(t, err)
@ -140,6 +143,14 @@ func TestNew(t *testing.T) {
require.NoError(t, err)
require.Equal(t, 2, len(a.signers))
require.Equal(t, 2, len(a.txSigners))
// Good tuned
opts := Options{
Attributes: []transaction.Attribute{{Type: transaction.HighPriority}},
}
a, err = NewTuned(client, signers, opts)
require.NoError(t, err)
require.Equal(t, 1, len(a.opts.Attributes))
}
func TestSimpleWrappers(t *testing.T) {

View file

@ -33,37 +33,53 @@ type TransactionCheckerModifier func(r *result.Invoke, t *transaction.Transactio
// successfully accepted and executed.
type TransactionModifier func(t *transaction.Transaction) error
// DefaultModifier is the default modifier, it does nothing.
func DefaultModifier(t *transaction.Transaction) error {
return nil
}
// DefaultCheckerModifier is the default TransactionCheckerModifier, it checks
// for HALT state in the invocation result given to it and does nothing else.
func DefaultCheckerModifier(r *result.Invoke, t *transaction.Transaction) error {
if r.State != vmstate.Halt.String() {
return fmt.Errorf("script failed (%s state) due to an error: %s", r.State, r.FaultException)
}
return nil
}
// 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.
// contract with the given parameters. Test call is performed and filtered through
// Actor-configured TransactionCheckerModifier. The resulting transaction has
// Actor-configured attributes added as well. If you need to override attributes
// and/or TransactionCheckerModifier 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.
// MakeTunedCall creates a transaction with the given attributes (or Actor default
// ones if nil) that calls the given method of the given contract with the given
// parameters. It's filtered through the provided callback (or Actor default
// one's if nil, see TransactionCheckerModifier documentation also), so the
// process can be aborted and transaction can be modified before signing.
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.
// invocation of this script is performed and filtered through Actor's
// TransactionCheckerModifier. The resulting transaction has attributes that are
// configured for current Actor. If you need to override them or use a different
// TransactionCheckerModifier 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.
// MakeTunedRun creates a transaction with the given attributes (or Actor default
// ones if nil) that executes the given script. It's filtered through the
// provided callback (if not nil, otherwise Actor default one is used, see
// TransactionCheckerModifier documentation also), so the process can be aborted
// and transaction can be modified before signing.
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)
@ -75,32 +91,31 @@ func (a *Actor) makeUncheckedWrapper(r *result.Invoke, err error, attrs []transa
}
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
txHook = a.opts.CheckerModifier
}
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.
// MakeUncheckedRun creates a transaction with the given attributes (or Actor
// default ones if nil) 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 (or Actor default one, see TransactionModifier documentation
// also), 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
}
if txHook == nil {
txHook = a.opts.Modifier
}
err = txHook(tx)
if err != nil {
return nil, err
}
err = a.Sign(tx)
if err != nil {
@ -113,6 +128,8 @@ func (a *Actor) MakeUncheckedRun(script []byte, sysfee int64, attrs []transactio
// 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.
// TransactionModifier is not applied to the result of this method, but default
// attributes are used if attrs is nil.
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)
@ -121,7 +138,8 @@ func (a *Actor) MakeUnsignedCall(contract util.Uint160, method string, attrs []t
// 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.
// NetworkFee values. TransactionModifier is not applied to the result of this
// method, but default attributes are used if attrs is nil.
func (a *Actor) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) {
r, err := a.Run(script)
return a.makeUnsignedWrapper(r, err, attrs)
@ -131,8 +149,9 @@ func (a *Actor) makeUnsignedWrapper(r *result.Invoke, err error, attrs []transac
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)
err = DefaultCheckerModifier(r, nil) // We know it doesn't care about transaction anyway.
if err != nil {
return nil, err
}
return a.MakeUnsignedUncheckedRun(r.Script, r.GasConsumed, attrs)
}
@ -144,7 +163,8 @@ func (a *Actor) makeUnsignedWrapper(r *result.Invoke, err error, attrs []transac
// 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.
// exchanged via context.ParameterContext. TransactionModifier is not applied to
// the result of this method, but default attributes are used if attrs is nil.
func (a *Actor) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error) {
var err error
@ -155,6 +175,9 @@ func (a *Actor) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []tr
return nil, errors.New("negative system fee")
}
if attrs == nil {
attrs = a.opts.Attributes // Might as well be nil, but it's OK.
}
tx := transaction.New(script, sysFee)
tx.Signers = a.txSigners
tx.Attributes = attrs

View file

@ -90,6 +90,17 @@ func TestMakeUnsigned(t *testing.T) {
client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
_, err = a.MakeUnsignedRun(script, nil)
require.NoError(t, err)
// Tuned.
opts := Options{
Attributes: []transaction.Attribute{{Type: transaction.HighPriority}},
}
a, err = NewTuned(client, a.signers, opts)
require.NoError(t, err)
tx, err = a.MakeUnsignedRun(script, nil)
require.NoError(t, err)
require.True(t, tx.HasAttribute(transaction.HighPriority))
}
func TestMakeSigned(t *testing.T) {
@ -127,6 +138,20 @@ func TestMakeSigned(t *testing.T) {
require.NoError(t, err)
require.Equal(t, uint32(777), tx.ValidUntilBlock)
// Tuned.
opts := Options{
Modifier: func(t *transaction.Transaction) error {
t.ValidUntilBlock = 888
return nil
},
}
at, err := NewTuned(client, a.signers, opts)
require.NoError(t, err)
tx, err = at.MakeUncheckedRun(script, 0, nil, nil)
require.NoError(t, err)
require.Equal(t, uint32(888), tx.ValidUntilBlock)
// Checked
// Bad, invocation fails.
@ -175,4 +200,18 @@ func TestMakeSigned(t *testing.T) {
client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script}
_, err = a.MakeCall(util.Uint160{}, "method", 1)
require.NoError(t, err)
// Tuned.
opts = Options{
CheckerModifier: func(r *result.Invoke, t *transaction.Transaction) error {
t.ValidUntilBlock = 888
return nil
},
}
at, err = NewTuned(client, a.signers, opts)
require.NoError(t, err)
tx, err = at.MakeRun(script)
require.NoError(t, err)
require.Equal(t, uint32(888), tx.ValidUntilBlock)
}