github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/actor/maker.go (about)

     1  package actor
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
     8  	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
     9  	"github.com/nspcc-dev/neo-go/pkg/util"
    10  	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
    11  )
    12  
    13  // TransactionCheckerModifier is a callback that receives the result of
    14  // test-invocation and the transaction that can perform the same invocation
    15  // on chain. This callback is accepted by methods that create transactions, it
    16  // can examine both arguments and return an error if there is anything wrong
    17  // there which will abort the creation process. Notice that when used this
    18  // callback is completely responsible for invocation result checking, including
    19  // checking for HALT execution state (so if you don't check for it in a callback
    20  // you can send a transaction that is known to end up in FAULT state). It can
    21  // also modify the transaction (see TransactionModifier).
    22  type TransactionCheckerModifier func(r *result.Invoke, t *transaction.Transaction) error
    23  
    24  // TransactionModifier is a callback that receives the transaction before
    25  // it's signed from a method that creates signed transactions. It can check
    26  // fees and other fields of the transaction and return an error if there is
    27  // anything wrong there which will abort the creation process. It also can modify
    28  // Nonce, SystemFee, NetworkFee and ValidUntilBlock values taking full
    29  // responsibility on the effects of these modifications (smaller fee values, too
    30  // low or too high ValidUntilBlock or bad Nonce can render transaction invalid).
    31  // Modifying other fields is not supported. Mostly it's useful for increasing
    32  // fee values since by default they're just enough for transaction to be
    33  // successfully accepted and executed.
    34  type TransactionModifier func(t *transaction.Transaction) error
    35  
    36  // DefaultModifier is the default modifier, it does nothing.
    37  func DefaultModifier(t *transaction.Transaction) error {
    38  	return nil
    39  }
    40  
    41  // DefaultCheckerModifier is the default TransactionCheckerModifier, it checks
    42  // for HALT state in the invocation result given to it and does nothing else.
    43  func DefaultCheckerModifier(r *result.Invoke, t *transaction.Transaction) error {
    44  	if r.State != vmstate.Halt.String() {
    45  		return fmt.Errorf("script failed (%s state) due to an error: %s", r.State, r.FaultException)
    46  	}
    47  	return nil
    48  }
    49  
    50  // MakeCall creates a transaction that calls the given method of the given
    51  // contract with the given parameters. Test call is performed and filtered through
    52  // Actor-configured TransactionCheckerModifier. The resulting transaction has
    53  // Actor-configured attributes added as well. If you need to override attributes
    54  // and/or TransactionCheckerModifier use MakeTunedCall.
    55  func (a *Actor) MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) {
    56  	return a.MakeTunedCall(contract, method, nil, nil, params...)
    57  }
    58  
    59  // MakeTunedCall creates a transaction with the given attributes (or Actor default
    60  // ones if nil) that calls the given method of the given contract with the given
    61  // parameters. It's filtered through the provided callback (or Actor default
    62  // one's if nil, see TransactionCheckerModifier documentation also), so the
    63  // process can be aborted and transaction can be modified before signing.
    64  func (a *Actor) MakeTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...any) (*transaction.Transaction, error) {
    65  	r, err := a.Call(contract, method, params...)
    66  	return a.makeUncheckedWrapper(r, err, attrs, txHook)
    67  }
    68  
    69  // MakeRun creates a transaction with the given executable script. Test
    70  // invocation of this script is performed and filtered through Actor's
    71  // TransactionCheckerModifier. The resulting transaction has attributes that are
    72  // configured for current Actor. If you need to override them or use a different
    73  // TransactionCheckerModifier use MakeTunedRun.
    74  func (a *Actor) MakeRun(script []byte) (*transaction.Transaction, error) {
    75  	return a.MakeTunedRun(script, nil, nil)
    76  }
    77  
    78  // MakeTunedRun creates a transaction with the given attributes (or Actor default
    79  // ones if nil) that executes the given script. It's filtered through the
    80  // provided callback (if not nil, otherwise Actor default one is used, see
    81  // TransactionCheckerModifier documentation also), so the process can be aborted
    82  // and transaction can be modified before signing.
    83  func (a *Actor) MakeTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) {
    84  	r, err := a.Run(script)
    85  	return a.makeUncheckedWrapper(r, err, attrs, txHook)
    86  }
    87  
    88  func (a *Actor) makeUncheckedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) {
    89  	if err != nil {
    90  		return nil, fmt.Errorf("test invocation failed: %w", err)
    91  	}
    92  	return a.MakeUncheckedRun(r.Script, r.GasConsumed, attrs, func(tx *transaction.Transaction) error {
    93  		if txHook == nil {
    94  			txHook = a.opts.CheckerModifier
    95  		}
    96  		return txHook(r, tx)
    97  	})
    98  }
    99  
   100  // MakeUncheckedRun creates a transaction with the given attributes (or Actor
   101  // default ones if nil) that executes the given script and is expected to use
   102  // up to sysfee GAS for its execution. The transaction is filtered through the
   103  // provided callback (or Actor default one, see TransactionModifier documentation
   104  // also), so the process can be aborted and transaction can be modified before
   105  // signing. This method is mostly useful when test invocation is already
   106  // performed and the script and required system fee values are already known.
   107  func (a *Actor) MakeUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (*transaction.Transaction, error) {
   108  	tx, err := a.MakeUnsignedUncheckedRun(script, sysfee, attrs)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	if txHook == nil {
   114  		txHook = a.opts.Modifier
   115  	}
   116  	err = txHook(tx)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	err = a.Sign(tx)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  	return tx, nil
   125  }
   126  
   127  // MakeUnsignedCall creates an unsigned transaction with the given attributes
   128  // that calls the given method of the given contract with the given parameters.
   129  // Test-invocation is performed and is expected to end up in HALT state, the
   130  // transaction returned has correct SystemFee and NetworkFee values.
   131  // TransactionModifier is not applied to the result of this method, but default
   132  // attributes are used if attrs is nil.
   133  func (a *Actor) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) {
   134  	r, err := a.Call(contract, method, params...)
   135  	return a.makeUnsignedWrapper(r, err, attrs)
   136  }
   137  
   138  // MakeUnsignedRun creates an unsigned transaction with the given attributes
   139  // that executes the given script. Test-invocation is performed and is expected
   140  // to end up in HALT state, the transaction returned has correct SystemFee and
   141  // NetworkFee values. TransactionModifier is not applied to the result of this
   142  // method, but default attributes are used if attrs is nil.
   143  func (a *Actor) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) {
   144  	r, err := a.Run(script)
   145  	return a.makeUnsignedWrapper(r, err, attrs)
   146  }
   147  
   148  func (a *Actor) makeUnsignedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute) (*transaction.Transaction, error) {
   149  	if err != nil {
   150  		return nil, fmt.Errorf("failed to test-invoke: %w", err)
   151  	}
   152  	err = DefaultCheckerModifier(r, nil) // We know it doesn't care about transaction anyway.
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	return a.MakeUnsignedUncheckedRun(r.Script, r.GasConsumed, attrs)
   157  }
   158  
   159  // MakeUnsignedUncheckedRun creates an unsigned transaction containing the given
   160  // script with the system fee value and attributes. It's expected to be used when
   161  // test invocation is already done and the script and system fee value are already
   162  // known to be good, so it doesn't do test invocation internally. But it fills
   163  // Signers with Actor's signers, calculates proper ValidUntilBlock and NetworkFee
   164  // values. The resulting transaction can be changed in its Nonce, SystemFee,
   165  // NetworkFee and ValidUntilBlock values and then be signed and sent or
   166  // exchanged via context.ParameterContext. TransactionModifier is not applied to
   167  // the result of this method, but default attributes are used if attrs is nil.
   168  func (a *Actor) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error) {
   169  	var err error
   170  
   171  	if len(script) == 0 {
   172  		return nil, errors.New("empty script")
   173  	}
   174  	if sysFee < 0 {
   175  		return nil, errors.New("negative system fee")
   176  	}
   177  
   178  	if attrs == nil {
   179  		attrs = a.opts.Attributes // Might as well be nil, but it's OK.
   180  	}
   181  	tx := transaction.New(script, sysFee)
   182  	tx.Signers = a.txSigners
   183  	tx.Attributes = attrs
   184  
   185  	tx.ValidUntilBlock, err = a.CalculateValidUntilBlock()
   186  	if err != nil {
   187  		return nil, fmt.Errorf("calculating validUntilBlock: %w", err)
   188  	}
   189  
   190  	tx.Scripts = make([]transaction.Witness, len(a.signers))
   191  	for i := range a.signers {
   192  		if !a.signers[i].Account.Contract.Deployed {
   193  			tx.Scripts[i].VerificationScript = a.signers[i].Account.Contract.Script
   194  			continue
   195  		}
   196  		if build := a.signers[i].Account.Contract.InvocationBuilder; build != nil {
   197  			invoc, err := build(tx)
   198  			if err != nil {
   199  				return nil, fmt.Errorf("building witness for contract signer: %w", err)
   200  			}
   201  			tx.Scripts[i].InvocationScript = invoc
   202  		}
   203  	}
   204  	// CalculateNetworkFee doesn't call Hash or Size, only serializes the
   205  	// transaction via Bytes, so it's safe wrt internal caching.
   206  	tx.NetworkFee, err = a.client.CalculateNetworkFee(tx)
   207  	if err != nil {
   208  		return nil, fmt.Errorf("calculating network fee: %w", err)
   209  	}
   210  
   211  	return tx, nil
   212  }
   213  
   214  // CalculateValidUntilBlock returns correct ValidUntilBlock value for a new
   215  // transaction relative to the current blockchain height. It uses "height +
   216  // number of validators + 1" formula suggesting shorter transaction lifetime
   217  // than the usual "height + MaxValidUntilBlockIncrement" approach. Shorter
   218  // lifetime can be useful to control transaction acceptance wait time because
   219  // it can't be added into a block after ValidUntilBlock.
   220  func (a *Actor) CalculateValidUntilBlock() (uint32, error) {
   221  	blockCount, err := a.client.GetBlockCount()
   222  	if err != nil {
   223  		return 0, fmt.Errorf("can't get block count: %w", err)
   224  	}
   225  	var vc = uint32(a.version.Protocol.ValidatorsCount)
   226  	var bestH = uint32(0)
   227  	for h, n := range a.version.Protocol.ValidatorsHistory { // In case it's enabled.
   228  		if h >= bestH && h <= blockCount {
   229  			vc = n
   230  			bestH = h
   231  		}
   232  	}
   233  
   234  	return blockCount + vc + 1, nil
   235  }