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

     1  /*
     2  Package invoker provides a convenient wrapper to perform test calls via RPC client.
     3  
     4  This layer builds on top of the basic RPC client and simplifies performing
     5  test function invocations and script runs. It also makes historic calls (NeoGo
     6  extension) transparent, allowing to use the same API as for regular calls.
     7  Results of these calls can be interpreted by upper layer packages like actor
     8  (to create transactions) or unwrap (to retrieve data from return values).
     9  */
    10  package invoker
    11  
    12  import (
    13  	"errors"
    14  	"fmt"
    15  
    16  	"github.com/google/uuid"
    17  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    18  	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
    19  	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
    20  	"github.com/nspcc-dev/neo-go/pkg/util"
    21  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    22  )
    23  
    24  // DefaultIteratorResultItems is the default number of results to
    25  // request from the iterator. Typically it's the same as server's
    26  // MaxIteratorResultItems, but different servers can have different
    27  // settings.
    28  const DefaultIteratorResultItems = 100
    29  
    30  // RPCSessions is a set of RPC methods needed to retrieve values from the
    31  // session-based iterators.
    32  type RPCSessions interface {
    33  	TerminateSession(sessionID uuid.UUID) (bool, error)
    34  	TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error)
    35  }
    36  
    37  // RPCInvoke is a set of RPC methods needed to execute things at the current
    38  // blockchain height.
    39  type RPCInvoke interface {
    40  	RPCSessions
    41  
    42  	InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error)
    43  	InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error)
    44  	InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error)
    45  }
    46  
    47  // RPCInvokeHistoric is a set of RPC methods needed to execute things at some
    48  // fixed point in blockchain's life.
    49  type RPCInvokeHistoric interface {
    50  	RPCSessions
    51  
    52  	InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error)
    53  	InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error)
    54  	InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error)
    55  	InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error)
    56  	InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error)
    57  	InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error)
    58  }
    59  
    60  // Invoker allows to test-execute things using RPC client. Its API simplifies
    61  // reusing the same signers list for a series of invocations and at the
    62  // same time uses regular Go types for call parameters. It doesn't do anything with
    63  // the result of invocation, that's left for upper (contract) layer to deal with.
    64  // Invoker does not produce any transactions and does not change the state of the
    65  // chain.
    66  type Invoker struct {
    67  	client  RPCInvoke
    68  	signers []transaction.Signer
    69  }
    70  
    71  type historicConverter struct {
    72  	client RPCInvokeHistoric
    73  	height *uint32
    74  	root   *util.Uint256
    75  }
    76  
    77  // New creates an Invoker to test-execute things at the current blockchain height.
    78  // If you only want to read data from the contract using its safe methods normally
    79  // (but contract-specific in general case) it's OK to pass nil for signers (that
    80  // is, use no signers).
    81  func New(client RPCInvoke, signers []transaction.Signer) *Invoker {
    82  	return &Invoker{client, signers}
    83  }
    84  
    85  // NewHistoricAtHeight creates an Invoker to test-execute things at some given height.
    86  func NewHistoricAtHeight(height uint32, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker {
    87  	return New(&historicConverter{
    88  		client: client,
    89  		height: &height,
    90  	}, signers)
    91  }
    92  
    93  // NewHistoricWithState creates an Invoker to test-execute things with some
    94  // given state or block.
    95  func NewHistoricWithState(rootOrBlock util.Uint256, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker {
    96  	return New(&historicConverter{
    97  		client: client,
    98  		root:   &rootOrBlock,
    99  	}, signers)
   100  }
   101  
   102  func (h *historicConverter) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
   103  	if h.height != nil {
   104  		return h.client.InvokeScriptAtHeight(*h.height, script, signers)
   105  	}
   106  	if h.root != nil {
   107  		return h.client.InvokeScriptWithState(*h.root, script, signers)
   108  	}
   109  	panic("uninitialized historicConverter")
   110  }
   111  
   112  func (h *historicConverter) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
   113  	if h.height != nil {
   114  		return h.client.InvokeFunctionAtHeight(*h.height, contract, operation, params, signers)
   115  	}
   116  	if h.root != nil {
   117  		return h.client.InvokeFunctionWithState(*h.root, contract, operation, params, signers)
   118  	}
   119  	panic("uninitialized historicConverter")
   120  }
   121  
   122  func (h *historicConverter) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
   123  	if h.height != nil {
   124  		return h.client.InvokeContractVerifyAtHeight(*h.height, contract, params, signers, witnesses...)
   125  	}
   126  	if h.root != nil {
   127  		return h.client.InvokeContractVerifyWithState(*h.root, contract, params, signers, witnesses...)
   128  	}
   129  	panic("uninitialized historicConverter")
   130  }
   131  
   132  func (h *historicConverter) TerminateSession(sessionID uuid.UUID) (bool, error) {
   133  	return h.client.TerminateSession(sessionID)
   134  }
   135  
   136  func (h *historicConverter) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
   137  	return h.client.TraverseIterator(sessionID, iteratorID, maxItemsCount)
   138  }
   139  
   140  // Call invokes a method of the contract with the given parameters (and
   141  // Invoker-specific list of signers) and returns the result as is.
   142  func (v *Invoker) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) {
   143  	ps, err := smartcontract.NewParametersFromValues(params...)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	return v.client.InvokeFunction(contract, operation, ps, v.signers)
   148  }
   149  
   150  // CallAndExpandIterator creates a script containing a call of the specified method
   151  // of a contract with given parameters (similar to how Call operates). But then this
   152  // script contains additional code that expects that the result of the first call is
   153  // an iterator. This iterator is traversed extracting values from it and adding them
   154  // into an array until maxItems is reached or iterator has no more elements. The
   155  // result of the whole script is an array containing up to maxResultItems elements
   156  // from the iterator returned from the contract's method call. This script is executed
   157  // using regular JSON-API (according to the way Iterator is set up).
   158  func (v *Invoker) CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...any) (*result.Invoke, error) {
   159  	bytes, err := smartcontract.CreateCallAndUnwrapIteratorScript(contract, method, maxItems, params...)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("iterator unwrapper script: %w", err)
   162  	}
   163  	return v.Run(bytes)
   164  }
   165  
   166  // Verify invokes contract's verify method in the verification context with
   167  // Invoker-specific signers and given witnesses and parameters.
   168  func (v *Invoker) Verify(contract util.Uint160, witnesses []transaction.Witness, params ...any) (*result.Invoke, error) {
   169  	ps, err := smartcontract.NewParametersFromValues(params...)
   170  	if err != nil {
   171  		return nil, err
   172  	}
   173  	return v.client.InvokeContractVerify(contract, ps, v.signers, witnesses...)
   174  }
   175  
   176  // Run executes given bytecode with Invoker-specific list of signers.
   177  func (v *Invoker) Run(script []byte) (*result.Invoke, error) {
   178  	return v.client.InvokeScript(script, v.signers)
   179  }
   180  
   181  // TerminateSession closes the given session, returning an error if anything
   182  // goes wrong. It's not strictly required to close the session (it'll expire on
   183  // the server anyway), but it helps to release server resources earlier.
   184  func (v *Invoker) TerminateSession(sessionID uuid.UUID) error {
   185  	return termSession(v.client, sessionID)
   186  }
   187  
   188  func termSession(rpc RPCSessions, sessionID uuid.UUID) error {
   189  	r, err := rpc.TerminateSession(sessionID)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	if !r {
   194  		return errors.New("terminatesession returned false")
   195  	}
   196  	return nil
   197  }
   198  
   199  // TraverseIterator allows to retrieve the next batch of items from the given
   200  // iterator in the given session (previously returned from Call or Run). It works
   201  // both with session-backed iterators and expanded ones (which one you have
   202  // depends on the RPC server). It can change the state of the iterator in the
   203  // process. If num <= 0 then DefaultIteratorResultItems number of elements is
   204  // requested. If result contains no elements, then either Iterator has no
   205  // elements or session was expired and terminated by the server.
   206  func (v *Invoker) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) {
   207  	return iterateNext(v.client, sessionID, iterator, num)
   208  }
   209  
   210  func iterateNext(rpc RPCSessions, sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) {
   211  	if num <= 0 {
   212  		num = DefaultIteratorResultItems
   213  	}
   214  
   215  	if iterator.ID != nil {
   216  		return rpc.TraverseIterator(sessionID, *iterator.ID, num)
   217  	}
   218  	if num > len(iterator.Values) {
   219  		num = len(iterator.Values)
   220  	}
   221  	items := iterator.Values[:num]
   222  	iterator.Values = iterator.Values[num:]
   223  
   224  	return items, nil
   225  }