diff --git a/cli/paramcontext/context.go b/cli/paramcontext/context.go index 361af66fe..5ebfe9efc 100644 --- a/cli/paramcontext/context.go +++ b/cli/paramcontext/context.go @@ -12,14 +12,9 @@ import ( "github.com/nspcc-dev/neo-go/pkg/wallet" ) -// validUntilBlockIncrement is the number of extra blocks to add to an exported transaction. -const validUntilBlockIncrement = 50 - // InitAndSave creates an incompletely signed transaction which can be used // as an input to `multisig sign`. func InitAndSave(net netmode.Magic, tx *transaction.Transaction, acc *wallet.Account, filename string) error { - // avoid fast transaction expiration - tx.ValidUntilBlock += validUntilBlockIncrement priv := acc.PrivateKey() pub := priv.PublicKey() sign := priv.SignHashable(uint32(net), tx) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 642a576e7..ee81daeb9 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/cli/flags" @@ -23,6 +24,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" @@ -663,6 +665,7 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, resp *result.Invoke sender util.Uint160 signAndPush = acc != nil + act *actor.Actor ) if signAndPush { gas = flags.Fixed8FromContext(ctx, "gas") @@ -683,75 +686,101 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, if err != nil { return sender, err } - + if signAndPush { + // This will eventually be handled in cmdargs.GetSignersAccounts. + asa := make([]actor.SignerAccount, 0, len(cosigners)+1) + asa = append(asa, actor.SignerAccount{ + Signer: transaction.Signer{ + Account: sender, + Scopes: transaction.None, + }, + Account: acc, + }) + for _, c := range cosignersAccounts { + if c.Signer.Account == sender { + asa[0].Signer = c.Signer + continue + } + asa = append(asa, actor.SignerAccount{ + Signer: c.Signer, + Account: c.Account, + }) + } + act, err = actor.New(c, asa) + if err != nil { + return sender, cli.NewExitError(fmt.Errorf("failed to create RPC actor: %w", err), 1) + } + } out := ctx.String("out") + // It's a bit easier to keep this as is (not using invoker.Invoker) + // during transition period. Mostly because of the need to convert params + // to []interface{}. resp, err = c.InvokeFunction(script, operation, params, cosigners) if err != nil { return sender, cli.NewExitError(err, 1) } if resp.State != "HALT" { errText := fmt.Sprintf("Warning: %s VM state returned from the RPC node: %s", resp.State, resp.FaultException) - if out == "" && !signAndPush { + if !signAndPush { return sender, cli.NewExitError(errText, 1) } action := "save" process := "Saving" - if signAndPush { - if out != "" { - action += "and send" - process += "and sending" - } else { - action = "send" - process = "Sending" - } + if out != "" { + action += "and send" + process += "and sending" + } else { + action = "send" + process = "Sending" } if !ctx.Bool("force") { return sender, cli.NewExitError(errText+".\nUse --force flag to "+action+" the transaction anyway.", 1) } fmt.Fprintln(ctx.App.Writer, errText+".\n"+process+" transaction...") } - if out != "" { - tx, err := c.CreateTxFromScript(resp.Script, acc, resp.GasConsumed+int64(sysgas), int64(gas), cosignersAccounts) - if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) - } - m, err := c.GetNetwork() - if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to save tx: %w", err), 1) - } - if err := paramcontext.InitAndSave(m, tx, acc, out); err != nil { - return sender, cli.NewExitError(err, 1) - } - fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) - return sender, nil - } - if signAndPush { - if len(resp.Script) == 0 { - return sender, cli.NewExitError(errors.New("no script returned from the RPC node"), 1) - } - tx, err := c.CreateTxFromScript(resp.Script, acc, resp.GasConsumed+int64(sysgas), int64(gas), cosignersAccounts) - if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) - } - if !ctx.Bool("force") { - err := input.ConfirmTx(ctx.App.Writer, tx) - if err != nil { - return sender, cli.NewExitError(err, 1) - } - } - txHash, err := c.SignAndPushTx(tx, acc, cosignersAccounts) - if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) - } - fmt.Fprintf(ctx.App.Writer, "Sent invocation transaction %s\n", txHash.StringLE()) - } else { + if !signAndPush { b, err := json.MarshalIndent(resp, "", " ") if err != nil { return sender, cli.NewExitError(err, 1) } fmt.Fprintln(ctx.App.Writer, string(b)) + } else { + if len(resp.Script) == 0 { + return sender, cli.NewExitError(errors.New("no script returned from the RPC node"), 1) + } + ver := act.GetVersion() + tx, err := act.MakeUnsignedUncheckedRun(resp.Script, resp.GasConsumed+int64(sysgas), nil) + if err != nil { + return sender, cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) + } + tx.NetworkFee += int64(gas) + if out != "" { + // Make a long-lived transaction, it's to be signed manually. + tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 + m := act.GetNetwork() + if err := paramcontext.InitAndSave(m, tx, acc, out); err != nil { + return sender, cli.NewExitError(err, 1) + } + fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) + } else { + if !ctx.Bool("force") { + promptTime := time.Now() + err := input.ConfirmTx(ctx.App.Writer, tx) + if err != nil { + return sender, cli.NewExitError(err, 1) + } + waitTime := time.Since(promptTime) + // Compensate for confirmation waiting. + tx.ValidUntilBlock += uint32((waitTime.Milliseconds() / int64(ver.Protocol.MillisecondsPerBlock))) + 1 + } + txHash, _, err := act.SignAndSend(tx) + if err != nil { + return sender, cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) + } + fmt.Fprintf(ctx.App.Writer, "Sent invocation transaction %s\n", txHash.StringLE()) + } } return sender, nil diff --git a/cli/wallet/nep11.go b/cli/wallet/nep11.go index 4be67dc5c..82154e056 100644 --- a/cli/wallet/nep11.go +++ b/cli/wallet/nep11.go @@ -280,6 +280,12 @@ func signAndSendNEP11Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet tx.SystemFee += int64(sysgas) if outFile := ctx.String("out"); outFile != "" { + ver, err := c.GetVersion() + if err != nil { + return cli.NewExitError(fmt.Errorf("RPC failure: %w", err), 1) + } + // Make a long-lived transaction, it's to be signed manually. + tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 m, err := c.GetNetwork() if err != nil { return cli.NewExitError(fmt.Errorf("failed to save tx: %w", err), 1) @@ -294,7 +300,7 @@ func signAndSendNEP11Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet return cli.NewExitError(err, 1) } } - _, err := c.SignAndPushTx(tx, acc, cosigners) + _, err := c.SignAndPushTx(tx, acc, cosigners) //nolint:staticcheck // SA1019: c.SignAndPushTx is deprecated if err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 948574809..43ff8d418 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -659,6 +659,12 @@ func signAndSendNEP17Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet tx.SystemFee += int64(sysgas) if outFile := ctx.String("out"); outFile != "" { + ver, err := c.GetVersion() + if err != nil { + return cli.NewExitError(fmt.Errorf("RPC failure: %w", err), 1) + } + // Make a long-lived transaction, it's to be signed manually. + tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 m, err := c.GetNetwork() if err != nil { return cli.NewExitError(fmt.Errorf("failed to save tx: %w", err), 1) @@ -673,7 +679,7 @@ func signAndSendNEP17Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet return cli.NewExitError(err, 1) } } - _, err := c.SignAndPushTx(tx, acc, cosigners) + _, err := c.SignAndPushTx(tx, acc, cosigners) //nolint:staticcheck // SA1019: c.SignAndPushTx is deprecated if err != nil { return cli.NewExitError(err, 1) } diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 649545691..5f6a5aeb7 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -127,7 +127,7 @@ func handleCandidate(ctx *cli.Context, method string, sysGas int64) error { if err != nil { return cli.NewExitError(err, 1) } - res, err := c.SignAndPushInvocationTx(script, acc, sysGas, gas, []rpcclient.SignerAccount{{ + res, err := c.SignAndPushInvocationTx(script, acc, sysGas, gas, []rpcclient.SignerAccount{{ //nolint:staticcheck // SA1019: c.SignAndPushInvocationTx is deprecated Signer: transaction.Signer{ Account: acc.Contract.ScriptHash(), Scopes: transaction.CalledByEntry, @@ -191,7 +191,7 @@ func handleVote(ctx *cli.Context) error { if err != nil { return cli.NewExitError(err, 1) } - res, err := c.SignAndPushInvocationTx(script, acc, -1, gas, []rpcclient.SignerAccount{{ + res, err := c.SignAndPushInvocationTx(script, acc, -1, gas, []rpcclient.SignerAccount{{ //nolint:staticcheck // SA1019: c.SignAndPushInvocationTx is deprecated Signer: transaction.Signer{ Account: acc.Contract.ScriptHash(), Scopes: transaction.CalledByEntry, diff --git a/docs/rpc.md b/docs/rpc.md index 1872ccfa3..a894a93f9 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -162,6 +162,15 @@ latest state synchronization point P the node working against, `LastUpdatedBlock` equals P. For NEP-11 NFTs `LastUpdatedBlock` is equal for all tokens of the same asset. +##### `getversion` + +NeoGo can return additional fields in the `protocol` object depending on the +extensions enabled. Specifically that's `p2psigextensions` and +`staterootinheader` booleans and `committeehistory` and `validatorshistory` +objects (that are effectively maps from stringified integers to other +integers. These fields are only returned when corresponding settings are +enabled in the server's protocol configuration. + ##### `getnep11transfers` and `getnep17transfers` `transfernotifyindex` is not tracked by NeoGo, thus this field is always zero. diff --git a/pkg/neorpc/result/version.go b/pkg/neorpc/result/version.go index e1f3b49e6..9f9102949 100644 --- a/pkg/neorpc/result/version.go +++ b/pkg/neorpc/result/version.go @@ -39,8 +39,18 @@ type ( MemoryPoolMaxTransactions int ValidatorsCount byte InitialGasDistribution fixedn.Fixed8 + + // Below are NeoGo-specific extensions to the protocol that are + // returned by the server in case they're enabled. + + // CommitteeHistory stores height:size map of the committee size. + CommitteeHistory map[uint32]int + // P2PSigExtensions is true when Notary subsystem is enabled on the network. + P2PSigExtensions bool // StateRootInHeader is true if state root is contained in block header. StateRootInHeader bool + // ValidatorsHistory stores height:size map of the validators count. + ValidatorsHistory map[uint32]int } ) @@ -67,7 +77,11 @@ type ( MemoryPoolMaxTransactions int `json:"memorypoolmaxtransactions"` ValidatorsCount byte `json:"validatorscount"` InitialGasDistribution int64 `json:"initialgasdistribution"` - StateRootInHeader bool `json:"staterootinheader,omitempty"` + + CommitteeHistory map[uint32]int `json:"committeehistory,omitempty"` + P2PSigExtensions bool `json:"p2psigextensions,omitempty"` + StateRootInHeader bool `json:"staterootinheader,omitempty"` + ValidatorsHistory map[uint32]int `json:"validatorshistory,omitempty"` } // versionUnmarshallerAux is an auxiliary struct used for Version JSON unmarshalling. @@ -92,7 +106,11 @@ type ( MemoryPoolMaxTransactions int `json:"memorypoolmaxtransactions"` ValidatorsCount byte `json:"validatorscount"` InitialGasDistribution json.RawMessage `json:"initialgasdistribution"` - StateRootInHeader bool `json:"staterootinheader,omitempty"` + + CommitteeHistory map[uint32]int `json:"committeehistory,omitempty"` + P2PSigExtensions bool `json:"p2psigextensions,omitempty"` + StateRootInHeader bool `json:"staterootinheader,omitempty"` + ValidatorsHistory map[uint32]int `json:"validatorshistory,omitempty"` } ) @@ -118,7 +136,11 @@ func (v *Version) MarshalJSON() ([]byte, error) { MemoryPoolMaxTransactions: v.Protocol.MemoryPoolMaxTransactions, ValidatorsCount: v.Protocol.ValidatorsCount, InitialGasDistribution: int64(v.Protocol.InitialGasDistribution), - StateRootInHeader: v.Protocol.StateRootInHeader, + + CommitteeHistory: v.Protocol.CommitteeHistory, + P2PSigExtensions: v.Protocol.P2PSigExtensions, + StateRootInHeader: v.Protocol.StateRootInHeader, + ValidatorsHistory: v.Protocol.ValidatorsHistory, }, StateRootInHeader: v.StateRootInHeader, } @@ -145,7 +167,10 @@ func (v *Version) UnmarshalJSON(data []byte) error { v.Protocol.MaxTransactionsPerBlock = aux.Protocol.MaxTransactionsPerBlock v.Protocol.MemoryPoolMaxTransactions = aux.Protocol.MemoryPoolMaxTransactions v.Protocol.ValidatorsCount = aux.Protocol.ValidatorsCount + v.Protocol.CommitteeHistory = aux.Protocol.CommitteeHistory + v.Protocol.P2PSigExtensions = aux.Protocol.P2PSigExtensions v.Protocol.StateRootInHeader = aux.Protocol.StateRootInHeader + v.Protocol.ValidatorsHistory = aux.Protocol.ValidatorsHistory v.StateRootInHeader = aux.StateRootInHeader if len(aux.Protocol.InitialGasDistribution) == 0 { return nil diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go new file mode 100644 index 000000000..abf79530f --- /dev/null +++ b/pkg/rpcclient/actor/actor.go @@ -0,0 +1,204 @@ +/* +Package actor provides a way to change chain state via RPC client. + +This layer builds on top of the basic RPC client and simplifies creating, +signing and sending transactions to the network (since that's the only way chain +state is changed). It's generic enough to be used for any contract that you may +want to invoke and contract-specific functions can build on top of it. +*/ +package actor + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +// RPCActor is an interface required from the RPC client to successfully +// create and send transactions. +type RPCActor interface { + invoker.RPCInvoke + + CalculateNetworkFee(tx *transaction.Transaction) (int64, error) + GetBlockCount() (uint32, error) + GetVersion() (*result.Version, error) + SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) +} + +// SignerAccount represents combination of the transaction.Signer and the +// corresponding wallet.Account. It's used to create and sign transactions, each +// transaction has a set of signers that must witness the transaction with their +// signatures. +type SignerAccount struct { + Signer transaction.Signer + Account *wallet.Account +} + +// Actor keeps a connection to the RPC endpoint and allows to perform +// state-changing actions (via transactions that can also be created without +// sending them to the network) on behalf of a set of signers. It also provides +// an Invoker interface to perform test calls with the same set of signers. +type Actor struct { + invoker.Invoker + + client RPCActor + signers []SignerAccount + txSigners []transaction.Signer + version *result.Version +} + +// New creates an Actor instance using the specified RPC interface and the set of +// signers with corresponding accounts. Every transaction created by this Actor +// will have this set of signers and all communication will be performed via this +// RPC. Upon Actor instance creation a GetVersion call is made and the result of +// it is cached forever (and used for internal purposes). +func New(ra RPCActor, signers []SignerAccount) (*Actor, error) { + if len(signers) < 1 { + return nil, errors.New("at least one signer (sender) is required") + } + invSigners := make([]transaction.Signer, len(signers)) + for i := range signers { + if signers[i].Account.Contract == nil { + return nil, fmt.Errorf("empty contract for account %s", signers[i].Account.Address) + } + if !signers[i].Account.Contract.Deployed && signers[i].Account.Contract.ScriptHash() != signers[i].Signer.Account { + return nil, fmt.Errorf("signer account doesn't match script hash for signer %s", signers[i].Account.Address) + } + + invSigners[i] = signers[i].Signer + } + inv := invoker.New(ra, invSigners) + version, err := ra.GetVersion() + if err != nil { + return nil, err + } + return &Actor{ + Invoker: *inv, + client: ra, + signers: signers, + txSigners: invSigners, + version: version, + }, nil +} + +// NewSimple makes it easier to create an Actor for the most widespread case +// when transactions have only one signer that uses CalledByEntry scope. When +// other scopes or multiple signers are needed use New. +func NewSimple(ra RPCActor, acc *wallet.Account) (*Actor, error) { + return New(ra, []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: acc, + }}) +} + +// CalculateNetworkFee wraps RPCActor's CalculateNetworkFee, making it available +// to Actor users directly. It returns network fee value for the given +// transaction. +func (a *Actor) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) { + return a.client.CalculateNetworkFee(tx) +} + +// GetBlockCount wraps RPCActor's GetBlockCount, making it available to +// Actor users directly. It returns current number of blocks in the chain. +func (a *Actor) GetBlockCount() (uint32, error) { + return a.client.GetBlockCount() +} + +// GetNetwork is a convenience method that returns the network's magic number. +func (a *Actor) GetNetwork() netmode.Magic { + return a.version.Protocol.Network +} + +// GetVersion returns version data from the RPC endpoint. +func (a *Actor) GetVersion() result.Version { + return *a.version +} + +// Send allows to send arbitrary prepared transaction to the network. It returns +// transaction hash and ValidUntilBlock value. +func (a *Actor) Send(tx *transaction.Transaction) (util.Uint256, uint32, error) { + h, err := a.client.SendRawTransaction(tx) + return h, tx.ValidUntilBlock, err +} + +// Sign adds signatures to arbitrary transaction using Actor signers wallets. +// Most of the time it shouldn't be used directly since it'll be successful only +// if the transaction is made using the same set of accounts as the one used +// for Actor creation. +func (a *Actor) Sign(tx *transaction.Transaction) error { + if len(tx.Signers) != len(a.signers) { + return errors.New("incorrect number of signers in the transaction") + } + for i, signer := range a.signers { + err := signer.Account.SignTx(a.GetNetwork(), tx) + if err != nil { // then account is non-contract-based and locked, but let's provide more detailed error + if paramNum := len(signer.Account.Contract.Parameters); paramNum != 0 && signer.Account.Contract.Deployed { + return fmt.Errorf("failed to add contract-based witness for signer #%d (%s): "+ + "%d parameters must be provided to construct invocation script", i, signer.Account.Address, paramNum) + } + return fmt.Errorf("failed to add witness for signer #%d (%s): account should be unlocked to add the signature. "+ + "Store partially-signed transaction and then use 'wallet sign' command to cosign it", i, signer.Account.Address) + } + } + return nil +} + +// SignAndSend signs arbitrary transaction (see also Sign) and sends it to the +// network. +func (a *Actor) SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error) { + return a.sendWrapper(tx, a.Sign(tx)) +} + +// sendWrapper simplifies wrapping methods that create transactions. +func (a *Actor) sendWrapper(tx *transaction.Transaction, err error) (util.Uint256, uint32, error) { + if err != nil { + return util.Uint256{}, 0, err + } + return a.Send(tx) +} + +// SendCall creates a transaction that calls the given method of the given +// contract with the given parameters (see also MakeCall) and sends it to the +// network. +func (a *Actor) SendCall(contract util.Uint160, method string, params ...interface{}) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeCall(contract, method, params...)) +} + +// SendTunedCall creates a transaction that calls the given method of the given +// contract with the given parameters (see also MakeTunedCall) and attributes, +// allowing to check for execution results of this call and modify transaction +// before it's signed; this transaction is then sent to the network. +func (a *Actor) SendTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...interface{}) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeTunedCall(contract, method, attrs, txHook, params...)) +} + +// SendRun creates a transaction with the given executable script (see also +// MakeRun) and sends it to the network. +func (a *Actor) SendRun(script []byte) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeRun(script)) +} + +// SendTunedRun creates a transaction with the given executable script and +// attributes, allowing to check for execution results of this script and modify +// transaction before it's signed (see also MakeTunedRun). This transaction is +// then sent to the network. +func (a *Actor) SendTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeTunedRun(script, attrs, txHook)) +} + +// SendUncheckedRun creates a transaction with the given executable script and +// attributes that can use up to sysfee GAS for its execution, allowing to modify +// this transaction before it's signed (see also MakeUncheckedRun). This +// transaction is then sent to the network. +func (a *Actor) SendUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (util.Uint256, uint32, error) { + return a.sendWrapper(a.MakeUncheckedRun(script, sysfee, attrs, txHook)) +} diff --git a/pkg/rpcclient/actor/actor_test.go b/pkg/rpcclient/actor/actor_test.go new file mode 100644 index 000000000..46cdd3906 --- /dev/null +++ b/pkg/rpcclient/actor/actor_test.go @@ -0,0 +1,254 @@ +package actor + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/config/netmode" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/wallet" + "github.com/stretchr/testify/require" +) + +type RPCClient struct { + err error + invRes *result.Invoke + netFee int64 + bCount uint32 + version *result.Version + hash util.Uint256 +} + +func (r *RPCClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) { + return r.invRes, r.err +} +func (r *RPCClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) { + return r.netFee, r.err +} +func (r *RPCClient) GetBlockCount() (uint32, error) { + return r.bCount, r.err +} +func (r *RPCClient) GetVersion() (*result.Version, error) { + verCopy := *r.version + return &verCopy, r.err +} +func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) { + return r.hash, r.err +} + +func testRPCAndAccount(t *testing.T) (*RPCClient, *wallet.Account) { + client := &RPCClient{ + version: &result.Version{ + Protocol: result.Protocol{ + Network: netmode.UnitTestNet, + MillisecondsPerBlock: 1000, + ValidatorsCount: 7, + }, + }, + } + acc, err := wallet.NewAccount() + require.NoError(t, err) + return client, acc +} + +func TestNew(t *testing.T) { + client, acc := testRPCAndAccount(t) + + // No signers. + _, err := New(client, nil) + require.Error(t, err) + + _, err = New(client, []SignerAccount{}) + require.Error(t, err) + + // Good simple. + a, err := NewSimple(client, acc) + require.NoError(t, err) + require.Equal(t, 1, len(a.signers)) + require.Equal(t, 1, len(a.txSigners)) + require.Equal(t, transaction.CalledByEntry, a.signers[0].Signer.Scopes) + require.Equal(t, transaction.CalledByEntry, a.txSigners[0].Scopes) + + // Contractless account. + badAcc, err := wallet.NewAccount() + require.NoError(t, err) + badAccHash := badAcc.Contract.ScriptHash() + badAcc.Contract = nil + + signers := []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc, + }, { + Signer: transaction.Signer{ + Account: badAccHash, + Scopes: transaction.CalledByEntry, + }, + Account: badAcc, + }} + + _, err = New(client, signers) + require.Error(t, err) + + // GetVersion returning error. + client.err = errors.New("bad") + _, err = NewSimple(client, acc) + require.Error(t, err) + client.err = nil + + // Account mismatch. + acc2, err := wallet.NewAccount() + require.NoError(t, err) + signers = []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc2.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc, + }, { + Signer: transaction.Signer{ + Account: acc2.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: acc2, + }} + _, err = New(client, signers) + require.Error(t, err) + + // Good multiaccount. + signers[0].Signer.Account = acc.Contract.ScriptHash() + a, err = New(client, signers) + require.NoError(t, err) + require.Equal(t, 2, len(a.signers)) + require.Equal(t, 2, len(a.txSigners)) +} + +func TestSimpleWrappers(t *testing.T) { + client, acc := testRPCAndAccount(t) + origVer := *client.version + + a, err := NewSimple(client, acc) + require.NoError(t, err) + + client.netFee = 42 + nf, err := a.CalculateNetworkFee(new(transaction.Transaction)) + require.NoError(t, err) + require.Equal(t, int64(42), nf) + + client.bCount = 100500 + bc, err := a.GetBlockCount() + require.NoError(t, err) + require.Equal(t, uint32(100500), bc) + + require.Equal(t, netmode.UnitTestNet, a.GetNetwork()) + client.version.Protocol.Network = netmode.TestNet + require.Equal(t, netmode.UnitTestNet, a.GetNetwork()) + require.Equal(t, origVer, a.GetVersion()) + + a, err = NewSimple(client, acc) + require.NoError(t, err) + require.Equal(t, netmode.TestNet, a.GetNetwork()) + require.Equal(t, *client.version, a.GetVersion()) + + client.hash = util.Uint256{1, 2, 3} + h, vub, err := a.Send(&transaction.Transaction{ValidUntilBlock: 123}) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(123), vub) +} + +func TestSign(t *testing.T) { + client, acc := testRPCAndAccount(t) + acc2, err := wallet.NewAccount() + require.NoError(t, err) + + a, err := New(client, []SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.None, + }, + Account: acc, + }, { + Signer: transaction.Signer{ + Account: acc2.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: &wallet.Account{ // Looks like acc2, but has no private key. + Address: acc2.Address, + EncryptedWIF: acc2.EncryptedWIF, + Contract: acc2.Contract, + }, + }}) + require.NoError(t, err) + + script := []byte{1, 2, 3} + client.hash = util.Uint256{2, 5, 6} + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + + tx, err := a.MakeUnsignedRun(script, nil) + require.NoError(t, err) + require.Error(t, a.Sign(tx)) + _, _, err = a.SignAndSend(tx) + require.Error(t, err) +} + +func TestSenders(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + script := []byte{1, 2, 3} + + // Bad. + client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script} + _, _, err = a.SendCall(util.Uint160{1}, "method", 42) + require.Error(t, err) + _, _, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42) + require.Error(t, err) + _, _, err = a.SendRun(script) + require.Error(t, err) + _, _, err = a.SendTunedRun(script, nil, nil) + require.Error(t, err) + _, _, err = a.SendUncheckedRun(script, 1, nil, func(t *transaction.Transaction) error { + return errors.New("bad") + }) + require.Error(t, err) + + // Good. + client.hash = util.Uint256{2, 5, 6} + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + h, vub, err := a.SendCall(util.Uint160{1}, "method", 42) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendTunedCall(util.Uint160{1}, "method", nil, nil, 42) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendRun(script) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendTunedRun(script, nil, nil) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) + + h, vub, err = a.SendUncheckedRun(script, 1, nil, nil) + require.NoError(t, err) + require.Equal(t, client.hash, h) + require.Equal(t, uint32(8), vub) +} diff --git a/pkg/rpcclient/actor/maker.go b/pkg/rpcclient/actor/maker.go new file mode 100644 index 000000000..392a84391 --- /dev/null +++ b/pkg/rpcclient/actor/maker.go @@ -0,0 +1,204 @@ +package actor + +import ( + "errors" + "fmt" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" +) + +// TransactionCheckerModifier is a callback that receives the result of +// test-invocation and the transaction that can perform the same invocation +// on chain. This callback is accepted by methods that create transactions, it +// can examine both arguments and return an error if there is anything wrong +// there which will abort the creation process. Notice that when used this +// callback is completely responsible for invocation result checking, including +// checking for HALT execution state (so if you don't check for it in a callback +// you can send a transaction that is known to end up in FAULT state). It can +// also modify the transaction (see TransactionModifier). +type TransactionCheckerModifier func(r *result.Invoke, t *transaction.Transaction) error + +// TransactionModifier is a callback that receives the transaction before +// it's signed from a method that creates signed transactions. It can check +// fees and other fields of the transaction and return an error if there is +// anything wrong there which will abort the creation process. It also can modify +// Nonce, SystemFee, NetworkFee and ValidUntilBlock values taking full +// responsibility on the effects of these modifications (smaller fee values, too +// low or too high ValidUntilBlock or bad Nonce can render transaction invalid). +// Modifying other fields is not supported. Mostly it's useful for increasing +// fee values since by default they're just enough for transaction to be +// successfully accepted and executed. +type TransactionModifier func(t *transaction.Transaction) error + +// MakeCall creates a transaction that calls the given method of the given +// contract with the given parameters. Test call is performed and checked for +// HALT status, if more checks are needed or transaction should have some +// additional attributes use MakeTunedCall. +func (a *Actor) MakeCall(contract util.Uint160, method string, params ...interface{}) (*transaction.Transaction, error) { + return a.MakeTunedCall(contract, method, nil, nil, params...) +} + +// MakeTunedCall creates a transaction with the given attributes that calls the +// given method of the given contract with the given parameters. It's filtered +// through the provided callback (see TransactionCheckerModifier documentation), +// so the process can be aborted and transaction can be modified before signing. +// If no callback is given then the result is checked for HALT state. +func (a *Actor) MakeTunedCall(contract util.Uint160, method string, attrs []transaction.Attribute, txHook TransactionCheckerModifier, params ...interface{}) (*transaction.Transaction, error) { + r, err := a.Call(contract, method, params...) + return a.makeUncheckedWrapper(r, err, attrs, txHook) +} + +// MakeRun creates a transaction with the given executable script. Test +// invocation of this script is performed and expected to end up in HALT +// state. If more checks are needed or transaction should have some additional +// attributes use MakeTunedRun. +func (a *Actor) MakeRun(script []byte) (*transaction.Transaction, error) { + return a.MakeTunedRun(script, nil, nil) +} + +// MakeTunedRun creates a transaction with the given attributes that executes +// the given script. It's filtered through the provided callback (see +// TransactionCheckerModifier documentation), so the process can be aborted and +// transaction can be modified before signing. If no callback is given then the +// result is checked for HALT state. +func (a *Actor) MakeTunedRun(script []byte, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) { + r, err := a.Run(script) + return a.makeUncheckedWrapper(r, err, attrs, txHook) +} + +func (a *Actor) makeUncheckedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute, txHook TransactionCheckerModifier) (*transaction.Transaction, error) { + if err != nil { + return nil, fmt.Errorf("test invocation failed: %w", err) + } + return a.MakeUncheckedRun(r.Script, r.GasConsumed, attrs, func(tx *transaction.Transaction) error { + if txHook == nil { + if r.State != vmstate.Halt.String() { + return fmt.Errorf("script failed (%s state) due to an error: %s", r.State, r.FaultException) + } + return nil + } + return txHook(r, tx) + }) +} + +// MakeUncheckedRun creates a transaction with the given attributes that executes +// the given script and is expected to use up to sysfee GAS for its execution. +// The transaction is filtered through the provided callback (see +// TransactionModifier documentation), so the process can be aborted and +// transaction can be modified before signing. This method is mostly useful when +// test invocation is already performed and the script and required system fee +// values are already known. +func (a *Actor) MakeUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (*transaction.Transaction, error) { + tx, err := a.MakeUnsignedUncheckedRun(script, sysfee, attrs) + if err != nil { + return nil, err + } + if txHook != nil { + err = txHook(tx) + if err != nil { + return nil, err + } + } + err = a.Sign(tx) + if err != nil { + return nil, err + } + return tx, nil +} + +// MakeUnsignedCall creates an unsigned transaction with the given attributes +// that calls the given method of the given contract with the given parameters. +// Test-invocation is performed and is expected to end up in HALT state, the +// transaction returned has correct SystemFee and NetworkFee values. +func (a *Actor) MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...interface{}) (*transaction.Transaction, error) { + r, err := a.Call(contract, method, params...) + return a.makeUnsignedWrapper(r, err, attrs) +} + +// MakeUnsignedRun creates an unsigned transaction with the given attributes +// that executes the given script. Test-invocation is performed and is expected +// to end up in HALT state, the transaction returned has correct SystemFee and +// NetworkFee values. +func (a *Actor) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) { + r, err := a.Run(script) + return a.makeUnsignedWrapper(r, err, attrs) +} + +func (a *Actor) makeUnsignedWrapper(r *result.Invoke, err error, attrs []transaction.Attribute) (*transaction.Transaction, error) { + if err != nil { + return nil, fmt.Errorf("failed to test-invoke: %w", err) + } + if r.State != vmstate.Halt.String() { + return nil, fmt.Errorf("test invocation faulted (%s): %s", r.State, r.FaultException) + } + return a.MakeUnsignedUncheckedRun(r.Script, r.GasConsumed, attrs) +} + +// MakeUnsignedUncheckedRun creates an unsigned transaction containing the given +// script with the system fee value and attributes. It's expected to be used when +// test invocation is already done and the script and system fee value are already +// known to be good, so it doesn't do test invocation internally. But it fills +// Signers with Actor's signers, calculates proper ValidUntilBlock and NetworkFee +// values. The resulting transaction can be changed in its Nonce, SystemFee, +// NetworkFee and ValidUntilBlock values and then be signed and sent or +// exchanged via context.ParameterContext. +func (a *Actor) MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error) { + var err error + + if len(script) == 0 { + return nil, errors.New("empty script") + } + if sysFee < 0 { + return nil, errors.New("negative system fee") + } + + tx := transaction.New(script, sysFee) + tx.Signers = a.txSigners + tx.Attributes = attrs + + tx.ValidUntilBlock, err = a.CalculateValidUntilBlock() + if err != nil { + return nil, fmt.Errorf("calculating validUntilBlock: %w", err) + } + + tx.Scripts = make([]transaction.Witness, len(a.signers)) + for i := range a.signers { + if !a.signers[i].Account.Contract.Deployed { + tx.Scripts[i].VerificationScript = a.signers[i].Account.Contract.Script + } + } + // CalculateNetworkFee doesn't call Hash or Size, only serializes the + // transaction via Bytes, so it's safe wrt internal caching. + tx.NetworkFee, err = a.client.CalculateNetworkFee(tx) + if err != nil { + return nil, fmt.Errorf("calculating network fee: %w", err) + } + + return tx, nil +} + +// CalculateValidUntilBlock returns correct ValidUntilBlock value for a new +// transaction relative to the current blockchain height. It uses "height + +// number of validators + 1" formula suggesting shorter transaction lifetime +// than the usual "height + MaxValidUntilBlockIncrement" approach. Shorter +// lifetime can be useful to control transaction acceptance wait time because +// it can't be added into a block after ValidUntilBlock. +func (a *Actor) CalculateValidUntilBlock() (uint32, error) { + blockCount, err := a.client.GetBlockCount() + if err != nil { + return 0, fmt.Errorf("can't get block count: %w", err) + } + var vc = int(a.version.Protocol.ValidatorsCount) + var bestH = uint32(0) + for h, n := range a.version.Protocol.ValidatorsHistory { // In case it's enabled. + if h >= bestH && h <= blockCount { + vc = n + bestH = h + } + } + + return blockCount + uint32(vc+1), nil +} diff --git a/pkg/rpcclient/actor/maker_test.go b/pkg/rpcclient/actor/maker_test.go new file mode 100644 index 000000000..116baf46b --- /dev/null +++ b/pkg/rpcclient/actor/maker_test.go @@ -0,0 +1,178 @@ +package actor + +import ( + "errors" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +func TestCalculateValidUntilBlock(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + + client.err = errors.New("error") + _, err = a.CalculateValidUntilBlock() + require.Error(t, err) + + client.err = nil + client.bCount = 42 + vub, err := a.CalculateValidUntilBlock() + require.NoError(t, err) + require.Equal(t, uint32(42+7+1), vub) + + client.version.Protocol.ValidatorsHistory = map[uint32]int{ + 0: 7, + 40: 4, + 80: 10, + } + a, err = NewSimple(client, acc) + require.NoError(t, err) + + vub, err = a.CalculateValidUntilBlock() + require.NoError(t, err) + require.Equal(t, uint32(42+4+1), vub) + + client.bCount = 101 + vub, err = a.CalculateValidUntilBlock() + require.NoError(t, err) + require.Equal(t, uint32(101+10+1), vub) +} + +func TestMakeUnsigned(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + + // Bad parameters. + script := []byte{1, 2, 3} + _, err = a.MakeUnsignedUncheckedRun(script, -1, nil) + require.Error(t, err) + _, err = a.MakeUnsignedUncheckedRun([]byte{}, 1, nil) + require.Error(t, err) + _, err = a.MakeUnsignedUncheckedRun(nil, 1, nil) + require.Error(t, err) + + // RPC error. + client.err = errors.New("err") + _, err = a.MakeUnsignedUncheckedRun(script, 1, nil) + require.Error(t, err) + + // Good unchecked. + client.netFee = 42 + client.bCount = 100500 + client.err = nil + tx, err := a.MakeUnsignedUncheckedRun(script, 1, nil) + require.NoError(t, err) + require.Equal(t, script, tx.Script) + require.Equal(t, 1, len(tx.Signers)) + require.Equal(t, acc.Contract.ScriptHash(), tx.Signers[0].Account) + require.Equal(t, 1, len(tx.Scripts)) + require.Equal(t, acc.Contract.Script, tx.Scripts[0].VerificationScript) + require.Nil(t, tx.Scripts[0].InvocationScript) + + // Bad run. + client.err = errors.New("") + _, err = a.MakeUnsignedRun(script, nil) + require.Error(t, err) + + // Faulted run. + client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script} + client.err = nil + _, err = a.MakeUnsignedRun(script, nil) + require.Error(t, err) + + // Good run. + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + _, err = a.MakeUnsignedRun(script, nil) + require.NoError(t, err) +} + +func TestMakeSigned(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + + // Bad script. + _, err = a.MakeUncheckedRun(nil, 0, nil, nil) + require.Error(t, err) + + // Good, no hook. + script := []byte{1, 2, 3} + _, err = a.MakeUncheckedRun(script, 0, nil, nil) + require.NoError(t, err) + + // Bad, can't sign because of a hook. + _, err = a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error { + t.Signers = append(t.Signers, transaction.Signer{}) + return nil + }) + require.Error(t, err) + + // Bad, hook returns an error. + _, err = a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error { + return errors.New("") + }) + require.Error(t, err) + + // Good with a hook. + tx, err := a.MakeUncheckedRun(script, 0, nil, func(t *transaction.Transaction) error { + t.ValidUntilBlock = 777 + return nil + }) + require.NoError(t, err) + require.Equal(t, uint32(777), tx.ValidUntilBlock) + + // Checked + + // Bad, invocation fails. + client.err = errors.New("") + _, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error { + return nil + }) + require.Error(t, err) + + // Bad, hook returns an error. + client.err = nil + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + _, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error { + return errors.New("") + }) + require.Error(t, err) + + // Good, no hook. + _, err = a.MakeTunedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}}, nil) + require.NoError(t, err) + _, err = a.MakeRun(script) + require.NoError(t, err) + + // Bad, invocation returns FAULT. + client.invRes = &result.Invoke{State: "FAULT", GasConsumed: 3, Script: script} + _, err = a.MakeTunedRun(script, nil, nil) + require.Error(t, err) + + // Good, invocation returns FAULT, but callback ignores it. + _, err = a.MakeTunedRun(script, nil, func(r *result.Invoke, t *transaction.Transaction) error { + return nil + }) + require.NoError(t, err) + + // Good, via call and with a callback. + _, err = a.MakeTunedCall(util.Uint160{}, "something", []transaction.Attribute{{Type: transaction.HighPriority}}, func(r *result.Invoke, t *transaction.Transaction) error { + return nil + }, "param", 1) + require.NoError(t, err) + + // Bad, it still is a FAULT. + _, err = a.MakeCall(util.Uint160{}, "method") + require.Error(t, err) + + // Good. + client.invRes = &result.Invoke{State: "HALT", GasConsumed: 3, Script: script} + _, err = a.MakeCall(util.Uint160{}, "method", 1) + require.NoError(t, err) +} diff --git a/pkg/rpcclient/nep17.go b/pkg/rpcclient/nep17.go index 06d0a4209..329f88fca 100644 --- a/pkg/rpcclient/nep17.go +++ b/pkg/rpcclient/nep17.go @@ -97,6 +97,9 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, // CreateTxFromScript creates transaction and properly sets cosigners and NetworkFee. // If sysFee <= 0, it is determined via result of `invokescript` RPC. You should // initialize network magic with Init before calling CreateTxFromScript. +// +// Deprecated: please use actor.Actor API, this method will be removed in future +// versions. func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, netFee int64, cosigners []SignerAccount) (*transaction.Transaction, error) { signers, accounts, err := getSigners(acc, cosigners) diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index 85020d5b1..84333fb9d 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -755,6 +755,9 @@ func (c *Client) SubmitRawOracleResponse(ps []interface{}) error { // possible. It spends the amount of gas specified. It returns a hash of the // invocation transaction and an error. If one of the cosigners accounts is // neither contract-based nor unlocked, an error is returned. +// +// Deprecated: please use actor.Actor API, this method will be removed in future +// versions. func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee int64, netfee fixedn.Fixed8, cosigners []SignerAccount) (util.Uint256, error) { tx, err := c.CreateTxFromScript(script, acc, sysfee, int64(netfee), cosigners) if err != nil { @@ -767,6 +770,9 @@ func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sys // it to the chain. It returns a hash of the transaction and an error. If one of // the cosigners accounts is neither contract-based nor unlocked, an error is // returned. +// +// Deprecated: please use actor.Actor API, this method will be removed in future +// versions. func (c *Client) SignAndPushTx(tx *transaction.Transaction, acc *wallet.Account, cosigners []SignerAccount) (util.Uint256, error) { var ( txHash util.Uint256 @@ -1007,6 +1013,9 @@ func (c *Client) ValidateAddress(address string) error { // current blockchain height + number of validators. Number of validators // is the length of blockchain validators list got from GetNextBlockValidators() // method. Validators count is being cached and updated every 100 blocks. +// +// Deprecated: please use (*Actor).CalculateValidUntilBlock. This method will be +// removed in future versions. func (c *Client) CalculateValidUntilBlock() (uint32, error) { var ( result uint32 @@ -1040,6 +1049,9 @@ func (c *Client) CalculateValidUntilBlock() (uint32, error) { // AddNetworkFee adds network fee for each witness script and optional extra // network fee to transaction. `accs` is an array signer's accounts. +// +// Deprecated: please use CalculateNetworkFee or actor.Actor. This method will +// be removed in future versions. func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs ...*wallet.Account) error { if len(tx.Signers) != len(accs) { return errors.New("number of signers must match number of scripts") diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index a53ce077f..354bf38d8 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -116,7 +116,7 @@ func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { Account: accs[0].PrivateKey().GetScriptHash(), Scopes: transaction.CalledByEntry, }} - require.Error(t, c.AddNetworkFee(tx, extraFee, accs[0], accs[1])) + require.Error(t, c.AddNetworkFee(tx, extraFee, accs[0], accs[1])) //nolint:staticcheck // SA1019: c.AddNetworkFee is deprecated }) t.Run("Simple", func(t *testing.T) { acc0 := wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)) @@ -137,7 +137,7 @@ func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { require.NoError(t, err) tx.Scripts = nil - require.NoError(t, c.AddNetworkFee(tx, extraFee, acc0)) + require.NoError(t, c.AddNetworkFee(tx, extraFee, acc0)) //nolint:staticcheck // SA1019: c.AddNetworkFee is deprecated actual := tx.NetworkFee require.NoError(t, acc0.SignTx(testchain.Network(), tx)) @@ -203,7 +203,7 @@ func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { tx.Scripts = nil - require.NoError(t, c.AddNetworkFee(tx, extraFee, acc0, acc1)) + require.NoError(t, c.AddNetworkFee(tx, extraFee, acc0, acc1)) //nolint:staticcheck // SA1019: c.AddNetworkFee is deprecated actual := tx.NetworkFee require.NoError(t, acc0.SignTx(testchain.Network(), tx)) @@ -276,7 +276,7 @@ func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { require.NoError(t, err) tx.Scripts = nil - require.NoError(t, c.AddNetworkFee(tx, extraFee, acc0, acc1)) + require.NoError(t, c.AddNetworkFee(tx, extraFee, acc0, acc1)) //nolint:staticcheck // SA1019: c.AddNetworkFee is deprecated require.NoError(t, acc0.SignTx(testchain.Network(), tx)) tx.Scripts = append(tx.Scripts, transaction.Witness{}) require.Equal(t, tx.NetworkFee, actual+extraFee) @@ -315,7 +315,7 @@ func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { Scopes: transaction.Global, }, } - require.Error(t, c.AddNetworkFee(tx, 10, acc0, acc1)) + require.Error(t, c.AddNetworkFee(tx, 10, acc0, acc1)) //nolint:staticcheck // SA1019: c.AddNetworkFee is deprecated }) t.Run("InvalidContract", func(t *testing.T) { tx := newTx(t) @@ -330,7 +330,7 @@ func TestAddNetworkFeeCalculateNetworkFee(t *testing.T) { Scopes: transaction.Global, }, } - require.Error(t, c.AddNetworkFee(tx, 10, acc0, acc1)) + require.Error(t, c.AddNetworkFee(tx, 10, acc0, acc1)) //nolint:staticcheck // SA1019: c.AddNetworkFee is deprecated }) }) } @@ -458,7 +458,7 @@ func TestSignAndPushInvocationTx(t *testing.T) { t.Run("good", func(t *testing.T) { t.Run("signer0: sig", func(t *testing.T) { - h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ + h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ //nolint:staticcheck // SA1019: c.SignAndPushInvocationTx is deprecated { Signer: transaction.Signer{ Account: priv0.GetScriptHash(), @@ -471,7 +471,7 @@ func TestSignAndPushInvocationTx(t *testing.T) { check(t, h) }) t.Run("signer0: sig; signer1: sig", func(t *testing.T) { - h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ + h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ //nolint:staticcheck // SA1019: c.SignAndPushInvocationTx is deprecated { Signer: transaction.Signer{ Account: priv0.GetScriptHash(), @@ -491,7 +491,7 @@ func TestSignAndPushInvocationTx(t *testing.T) { check(t, h) }) t.Run("signer0: sig; signer1: contract-based paramless", func(t *testing.T) { - h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ + h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ //nolint:staticcheck // SA1019: c.SignAndPushInvocationTx is deprecated { Signer: transaction.Signer{ Account: priv0.GetScriptHash(), @@ -513,7 +513,7 @@ func TestSignAndPushInvocationTx(t *testing.T) { }) t.Run("error", func(t *testing.T) { t.Run("signer0: sig; signer1: contract-based with params", func(t *testing.T) { - _, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ + _, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ //nolint:staticcheck // SA1019: c.SignAndPushInvocationTx is deprecated { Signer: transaction.Signer{ Account: priv0.GetScriptHash(), @@ -541,7 +541,7 @@ func TestSignAndPushInvocationTx(t *testing.T) { Parameters: []wallet.ContractParam{{Name: "parameter0", Type: smartcontract.SignatureType}}, }, } - _, err = c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ + _, err = c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc0, 30, 0, []rpcclient.SignerAccount{ //nolint:staticcheck // SA1019: c.SignAndPushInvocationTx is deprecated { Signer: transaction.Signer{ Account: priv0.GetScriptHash(), @@ -693,7 +693,7 @@ func TestCreateTxFromScript(t *testing.T) { priv := testchain.PrivateKey(0) acc := wallet.NewAccountFromPrivateKey(priv) t.Run("NoSystemFee", func(t *testing.T) { - tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, -1, 10, nil) + tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, -1, 10, nil) //nolint:staticcheck // SA1019: c.CreateTxFromScript is deprecated require.NoError(t, err) require.True(t, tx.ValidUntilBlock > chain.BlockHeight()) require.EqualValues(t, 30, tx.SystemFee) // PUSH1 @@ -701,7 +701,7 @@ func TestCreateTxFromScript(t *testing.T) { require.Equal(t, acc.PrivateKey().GetScriptHash(), tx.Signers[0].Account) }) t.Run("ProvideSystemFee", func(t *testing.T) { - tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, 123, 10, nil) + tx, err := c.CreateTxFromScript([]byte{byte(opcode.PUSH1)}, acc, 123, 10, nil) //nolint:staticcheck // SA1019: c.CreateTxFromScript is deprecated require.NoError(t, err) require.True(t, tx.ValidUntilBlock > chain.BlockHeight()) require.EqualValues(t, 123, tx.SystemFee) diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go index b7534c13b..e67c801a0 100644 --- a/pkg/services/rpcsrv/server.go +++ b/pkg/services/rpcsrv/server.go @@ -694,7 +694,11 @@ func (s *Server) getVersion(_ params.Params) (interface{}, *neorpc.Error) { MemoryPoolMaxTransactions: cfg.MemPoolSize, ValidatorsCount: byte(cfg.GetNumOfCNs(s.chain.BlockHeight())), InitialGasDistribution: cfg.InitialGASSupply, - StateRootInHeader: cfg.StateRootInHeader, + + CommitteeHistory: cfg.CommitteeHistory, + P2PSigExtensions: cfg.P2PSigExtensions, + StateRootInHeader: cfg.StateRootInHeader, + ValidatorsHistory: cfg.ValidatorsHistory, }, }, nil } diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go index 77b73fd46..011ca030b 100644 --- a/pkg/services/rpcsrv/server_test.go +++ b/pkg/services/rpcsrv/server_test.go @@ -875,7 +875,11 @@ var rpcTestCases = map[string][]rpcTestCase{ require.EqualValues(t, cfg.MemPoolSize, resp.Protocol.MemoryPoolMaxTransactions) require.EqualValues(t, cfg.ValidatorsCount, resp.Protocol.ValidatorsCount) require.EqualValues(t, cfg.InitialGASSupply, resp.Protocol.InitialGasDistribution) - require.EqualValues(t, false, resp.Protocol.StateRootInHeader) + + require.Equal(t, 0, len(resp.Protocol.CommitteeHistory)) + require.True(t, resp.Protocol.P2PSigExtensions) // Yeah, notary is enabled. + require.False(t, resp.Protocol.StateRootInHeader) + require.Equal(t, 0, len(resp.Protocol.ValidatorsHistory)) }, }, }, diff --git a/pkg/smartcontract/doc_test.go b/pkg/smartcontract/doc_test.go index d1b7fe8cc..4dfdda4ca 100644 --- a/pkg/smartcontract/doc_test.go +++ b/pkg/smartcontract/doc_test.go @@ -5,8 +5,8 @@ import ( "encoding/hex" "github.com/nspcc-dev/neo-go/pkg/core/transaction" - "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -34,8 +34,8 @@ func ExampleBuilder() { w, _ := wallet.NewWalletFromFile("somewhere") // Assuming there is one Account inside - acc := w.Accounts[0] - from, _ := address.StringToUint160(acc.Address) + a, _ := actor.NewSimple(c, w.Accounts[0]) + from := w.Accounts[0].Contract.ScriptHash() // Assuming Contract is present. // Multiple transfers in a single script. If any of them fail whole script fails. b.InvokeWithAssert(neoHash, "transfer", from, util.Uint160{0x70}, 1, nil) @@ -44,6 +44,7 @@ func ExampleBuilder() { script, _ = b.Script() // The script can then be used to create transaction or to invoke via RPC. - txid, _ := c.SignAndPushInvocationTx(script, acc, -1, 0, nil) + txid, vub, _ := a.SendRun(script) _ = txid + _ = vub } diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index e4496fd20..dbd3c5342 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -303,6 +303,10 @@ func NewParameterFromValue(value interface{}) (Parameter, error) { case uint64: result.Type = IntegerType result.Value = new(big.Int).SetUint64(v) + case *Parameter: + result = *v + case Parameter: + result = v case util.Uint160: result.Type = Hash160Type case util.Uint256: @@ -321,6 +325,11 @@ func NewParameterFromValue(value interface{}) (Parameter, error) { } result.Type = ArrayType result.Value = arr + case []Parameter: + arr := make([]Parameter, len(v)) + copy(arr, v) + result.Type = ArrayType + result.Value = arr case []*keys.PublicKey: return NewParameterFromValue(keys.PublicKeys(v)) case keys.PublicKeys: diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index 181901876..72b1070f7 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -616,6 +616,16 @@ func TestParameterFromValue(t *testing.T) { expType: IntegerType, expVal: big.NewInt(100), }, + { + value: Parameter{ByteArrayType, []byte{1, 2, 3}}, + expType: ByteArrayType, + expVal: []byte{1, 2, 3}, + }, + { + value: &Parameter{ByteArrayType, []byte{1, 2, 3}}, + expType: ByteArrayType, + expVal: []byte{1, 2, 3}, + }, { value: util.Uint160{1, 2, 3}, expType: Hash160Type, @@ -641,6 +651,11 @@ func TestParameterFromValue(t *testing.T) { expType: ArrayType, expVal: []Parameter{{ByteArrayType, []byte{1, 2, 3}}, {ByteArrayType, []byte{3, 2, 1}}}, }, + { + value: []Parameter{{ByteArrayType, []byte{1, 2, 3}}, {ByteArrayType, []byte{3, 2, 1}}}, + expType: ArrayType, + expVal: []Parameter{{ByteArrayType, []byte{1, 2, 3}}, {ByteArrayType, []byte{3, 2, 1}}}, + }, { value: []*keys.PublicKey{pk1.PublicKey(), pk2.PublicKey()}, expType: ArrayType, diff --git a/pkg/wallet/account.go b/pkg/wallet/account.go index bc370eb23..7b1619707 100644 --- a/pkg/wallet/account.go +++ b/pkg/wallet/account.go @@ -85,7 +85,9 @@ func NewAccount() (*Account, error) { // SignTx signs transaction t and updates it's Witnesses. func (a *Account) SignTx(net netmode.Magic, t *transaction.Transaction) error { if len(a.Contract.Parameters) == 0 { - t.Scripts = append(t.Scripts, transaction.Witness{}) + if len(t.Signers) != len(t.Scripts) { // Sequential signing vs. existing scripts. + t.Scripts = append(t.Scripts, transaction.Witness{}) + } return nil } if a.privateKey == nil {