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 }