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 }