diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go index 07b436227..aed020c7f 100644 --- a/pkg/rpcclient/actor/actor.go +++ b/pkg/rpcclient/actor/actor.go @@ -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. diff --git a/pkg/rpcclient/actor/actor_test.go b/pkg/rpcclient/actor/actor_test.go index c87b127bd..5ec1b4c4f 100644 --- a/pkg/rpcclient/actor/actor_test.go +++ b/pkg/rpcclient/actor/actor_test.go @@ -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) { diff --git a/pkg/rpcclient/actor/maker.go b/pkg/rpcclient/actor/maker.go index 392a84391..d185dd1fd 100644 --- a/pkg/rpcclient/actor/maker.go +++ b/pkg/rpcclient/actor/maker.go @@ -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 diff --git a/pkg/rpcclient/actor/maker_test.go b/pkg/rpcclient/actor/maker_test.go index 116baf46b..b7f66c7b4 100644 --- a/pkg/rpcclient/actor/maker_test.go +++ b/pkg/rpcclient/actor/maker_test.go @@ -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) }