github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/actor/actor.go (about) 1 /* 2 Package actor provides a way to change chain state via RPC client. 3 4 This layer builds on top of the basic RPC client and [invoker] package, it 5 simplifies creating, signing and sending transactions to the network (since 6 that's the only way chain state is changed). It's generic enough to be used for 7 any contract that you may want to invoke and contract-specific functions can 8 build on top of it. 9 */ 10 package actor 11 12 import ( 13 "errors" 14 "fmt" 15 16 "github.com/nspcc-dev/neo-go/pkg/config/netmode" 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/rpcclient/invoker" 20 "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" 21 "github.com/nspcc-dev/neo-go/pkg/util" 22 "github.com/nspcc-dev/neo-go/pkg/wallet" 23 ) 24 25 // RPCActor is an interface required from the RPC client to successfully 26 // create and send transactions. 27 type RPCActor interface { 28 invoker.RPCInvoke 29 30 // CalculateNetworkFee calculates network fee for the given transaction. 31 // 32 // CalculateNetworkFee MUST NOT call state-changing methods (like Hash or Size) 33 // of the transaction through the passed pointer: make a copy if necessary. 34 CalculateNetworkFee(tx *transaction.Transaction) (int64, error) 35 GetBlockCount() (uint32, error) 36 GetVersion() (*result.Version, error) 37 SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) 38 } 39 40 // SignerAccount represents combination of the transaction.Signer and the 41 // corresponding wallet.Account. It's used to create and sign transactions, each 42 // transaction has a set of signers that must witness the transaction with their 43 // signatures. 44 type SignerAccount struct { 45 Signer transaction.Signer 46 Account *wallet.Account 47 } 48 49 // Actor keeps a connection to the RPC endpoint and allows to perform 50 // state-changing actions (via transactions that can also be created without 51 // sending them to the network) on behalf of a set of signers. It also provides 52 // an Invoker interface to perform test calls with the same set of signers. 53 // 54 // Actor-specific APIs follow the naming scheme set by Invoker in method 55 // suffixes. *Call methods operate with function calls and require a contract 56 // hash, a method and parameters if any. *Run methods operate with scripts and 57 // require a NeoVM script that will be used directly. Prefixes denote the 58 // action to be performed, "Make" prefix is used for methods that create 59 // transactions in various ways, while "Send" prefix is used by methods that 60 // directly transmit created transactions to the RPC server. 61 // 62 // Actor also provides a [waiter.Waiter] interface to wait until transaction will be 63 // accepted to the chain. Depending on the underlying RPCActor functionality, 64 // transaction awaiting can be performed via web-socket using RPC notifications 65 // subsystem with [waiter.EventBased], via regular RPC requests using a poll-based 66 // algorithm with [waiter.PollingBased] or can not be performed if RPCActor doesn't 67 // implement none of [waiter.RPCEventBased] and [waiter.RPCPollingBased] interfaces with 68 // [waiter.Null]. [waiter.ErrAwaitingNotSupported] will be returned on attempt to await the 69 // transaction in the latter case. [waiter.Waiter] uses context of the underlying RPCActor 70 // and interrupts transaction awaiting process if the context is done. 71 // [waiter.ErrContextDone] wrapped with the context's error will be returned in this case. 72 // Otherwise, transaction awaiting process is ended with ValidUntilBlock acceptance 73 // and [waiter.ErrTxNotAccepted] is returned if transaction wasn't accepted by this moment. 74 type Actor struct { 75 invoker.Invoker 76 waiter.Waiter 77 78 client RPCActor 79 opts Options 80 signers []SignerAccount 81 txSigners []transaction.Signer 82 version *result.Version 83 } 84 85 // Options are used to create Actor with non-standard transaction checkers or 86 // additional attributes to be applied for all transactions. 87 type Options struct { 88 // Attributes are set as is into every transaction created by Actor, 89 // unless they're explicitly set in a method call that accepts 90 // attributes (like MakeTuned* or MakeUnsigned*). 91 Attributes []transaction.Attribute 92 // CheckerModifier is used by any method that creates and signs a 93 // transaction inside (some of them provide ways to override this 94 // default, some don't). 95 CheckerModifier TransactionCheckerModifier 96 // Modifier is used only by MakeUncheckedRun to modify transaction 97 // before it's signed (other methods that perform test invocations 98 // use CheckerModifier). MakeUnsigned* methods do not run it. 99 Modifier TransactionModifier 100 } 101 102 // New creates an Actor instance using the specified RPC interface and the set of 103 // signers with corresponding accounts. Every transaction created by this Actor 104 // will have this set of signers and all communication will be performed via this 105 // RPC. Upon Actor instance creation a GetVersion call is made and the result of 106 // it is cached forever (and used for internal purposes). The actor will use 107 // default Options (which can be overridden using NewTuned). 108 func New(ra RPCActor, signers []SignerAccount) (*Actor, error) { 109 if len(signers) < 1 { 110 return nil, errors.New("at least one signer (sender) is required") 111 } 112 invSigners := make([]transaction.Signer, len(signers)) 113 for i := range signers { 114 if signers[i].Account.Contract == nil { 115 return nil, fmt.Errorf("empty contract for account %s", signers[i].Account.Address) 116 } 117 if !signers[i].Account.Contract.Deployed && signers[i].Account.Contract.ScriptHash() != signers[i].Signer.Account { 118 return nil, fmt.Errorf("signer account doesn't match script hash for signer %s", signers[i].Account.Address) 119 } 120 121 invSigners[i] = signers[i].Signer 122 } 123 inv := invoker.New(ra, invSigners) 124 version, err := ra.GetVersion() 125 if err != nil { 126 return nil, err 127 } 128 return &Actor{ 129 Invoker: *inv, 130 Waiter: waiter.New(ra, version), 131 client: ra, 132 opts: NewDefaultOptions(), 133 signers: signers, 134 txSigners: invSigners, 135 version: version, 136 }, nil 137 } 138 139 // NewSimple makes it easier to create an Actor for the most widespread case 140 // when transactions have only one signer that uses CalledByEntry scope. When 141 // other scopes or multiple signers are needed use New. 142 func NewSimple(ra RPCActor, acc *wallet.Account) (*Actor, error) { 143 return New(ra, []SignerAccount{{ 144 Signer: transaction.Signer{ 145 Account: acc.Contract.ScriptHash(), 146 Scopes: transaction.CalledByEntry, 147 }, 148 Account: acc, 149 }}) 150 } 151 152 // NewDefaultOptions returns Options that have no attributes and use the default 153 // TransactionCheckerModifier function (that checks for the invocation result to 154 // be in HALT state) and TransactionModifier (that does nothing). 155 func NewDefaultOptions() Options { 156 return Options{ 157 CheckerModifier: DefaultCheckerModifier, 158 Modifier: DefaultModifier, 159 } 160 } 161 162 // NewTuned creates an Actor that will use the specified Options as defaults when 163 // creating new transactions. If checker/modifier callbacks are not provided 164 // (nil), then default ones (from NewDefaultOptions) are used. 165 func NewTuned(ra RPCActor, signers []SignerAccount, opts Options) (*Actor, error) { 166 a, err := New(ra, signers) 167 if err != nil { 168 return nil, err 169 } 170 a.opts.Attributes = opts.Attributes 171 if opts.CheckerModifier != nil { 172 a.opts.CheckerModifier = opts.CheckerModifier 173 } 174 if opts.Modifier != nil { 175 a.opts.Modifier = opts.Modifier 176 } 177 return a, err 178 } 179 180 // CalculateNetworkFee wraps RPCActor's CalculateNetworkFee, making it available 181 // to Actor users directly. It returns network fee value for the given 182 // transaction. 183 func (a *Actor) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) { 184 return a.client.CalculateNetworkFee(tx) 185 } 186 187 // GetBlockCount wraps RPCActor's GetBlockCount, making it available to 188 // Actor users directly. It returns current number of blocks in the chain. 189 func (a *Actor) GetBlockCount() (uint32, error) { 190 return a.client.GetBlockCount() 191 } 192 193 // GetNetwork is a convenience method that returns the network's magic number. 194 func (a *Actor) GetNetwork() netmode.Magic { 195 return a.version.Protocol.Network 196 } 197 198 // GetVersion returns version data from the RPC endpoint. 199 func (a *Actor) GetVersion() result.Version { 200 return *a.version 201 } 202 203 // Send allows to send arbitrary prepared transaction to the network. It returns 204 // transaction hash and ValidUntilBlock value. 205 func (a *Actor) Send(tx *transaction.Transaction) (util.Uint256, uint32, error) { 206 h, err := a.client.SendRawTransaction(tx) 207 return h, tx.ValidUntilBlock, err 208 } 209 210 // Sign adds signatures to arbitrary transaction using Actor signers wallets. 211 // Most of the time it shouldn't be used directly since it'll be successful only 212 // if the transaction is made using the same set of accounts as the one used 213 // for Actor creation. 214 func (a *Actor) Sign(tx *transaction.Transaction) error { 215 if len(tx.Signers) != len(a.signers) { 216 return errors.New("incorrect number of signers in the transaction") 217 } 218 for i, signer := range a.signers { 219 err := signer.Account.SignTx(a.GetNetwork(), tx) 220 if err != nil { // then account is non-contract-based and locked, but let's provide more detailed error 221 if paramNum := len(signer.Account.Contract.Parameters); paramNum != 0 && signer.Account.Contract.Deployed { 222 return fmt.Errorf("failed to add contract-based witness for signer #%d (%s): "+ 223 "%d parameters must be provided to construct invocation script", i, signer.Account.Address, paramNum) 224 } 225 return fmt.Errorf("failed to add witness for signer #%d (%s): account should be unlocked to add the signature. "+ 226 "Store partially-signed transaction and then use 'wallet sign' command to cosign it", i, signer.Account.Address) 227 } 228 } 229 return nil 230 } 231 232 // SignAndSend signs arbitrary transaction (see also Sign) and sends it to the 233 // network. 234 func (a *Actor) SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error) { 235 return a.sendWrapper(tx, a.Sign(tx)) 236 } 237 238 // sendWrapper simplifies wrapping methods that create transactions. 239 func (a *Actor) sendWrapper(tx *transaction.Transaction, err error) (util.Uint256, uint32, error) { 240 if err != nil { 241 return util.Uint256{}, 0, err 242 } 243 return a.Send(tx) 244 } 245 246 // SendCall creates a transaction that calls the given method of the given 247 // contract with the given parameters (see also MakeCall) and sends it to the 248 // network. 249 func (a *Actor) SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) { 250 return a.sendWrapper(a.MakeCall(contract, method, params...)) 251 } 252 253 // SendTunedCall creates a transaction that calls the given method of the given 254 // contract with the given parameters (see also MakeTunedCall) and attributes, 255 // allowing to check for execution results of this call and modify transaction 256 // before it's signed; this transaction is then sent to the network. 257 func (a *Actor) SendTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...any) (util.Uint256, uint32, error) { 258 return a.sendWrapper(a.MakeTunedCall(contract, method, attrs, txHook, params...)) 259 } 260 261 // SendRun creates a transaction with the given executable script (see also 262 // MakeRun) and sends it to the network. 263 func (a *Actor) SendRun(script []byte) (util.Uint256, uint32, error) { 264 return a.sendWrapper(a.MakeRun(script)) 265 } 266 267 // SendTunedRun creates a transaction with the given executable script and 268 // attributes, allowing to check for execution results of this script and modify 269 // transaction before it's signed (see also MakeTunedRun). This transaction is 270 // then sent to the network. 271 func (a *Actor) SendTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (util.Uint256, uint32, error) { 272 return a.sendWrapper(a.MakeTunedRun(script, attrs, txHook)) 273 } 274 275 // SendUncheckedRun creates a transaction with the given executable script and 276 // attributes that can use up to sysfee GAS for its execution, allowing to modify 277 // this transaction before it's signed (see also MakeUncheckedRun). This 278 // transaction is then sent to the network. 279 func (a *Actor) SendUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (util.Uint256, uint32, error) { 280 return a.sendWrapper(a.MakeUncheckedRun(script, sysfee, attrs, txHook)) 281 } 282 283 // Sender return the sender address that will be used in transactions created 284 // by Actor. 285 func (a *Actor) Sender() util.Uint160 { 286 return a.txSigners[0].Account 287 }