From 28f1beff646498c1977bea397d6d4d0c5e024fd8 Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Mon, 18 Dec 2023 22:36:53 +0300 Subject: [PATCH 1/4] rpcclient: export NewWaiter function Change first argument of NewWaiter to be able to directly accept RPC Client and export for external usage. Refs #3244. Signed-off-by: Ekaterina Pavlova --- pkg/rpcclient/actor/actor.go | 2 +- pkg/rpcclient/actor/waiter.go | 13 ++++++++----- pkg/rpcclient/actor/waiter_test.go | 10 +++++----- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go index 35ae9346a..bf3ec334e 100644 --- a/pkg/rpcclient/actor/actor.go +++ b/pkg/rpcclient/actor/actor.go @@ -126,7 +126,7 @@ func New(ra RPCActor, signers []SignerAccount) (*Actor, error) { } return &Actor{ Invoker: *inv, - Waiter: newWaiter(ra, version), + Waiter: NewWaiter(ra, version), client: ra, opts: NewDefaultOptions(), signers: signers, diff --git a/pkg/rpcclient/actor/waiter.go b/pkg/rpcclient/actor/waiter.go index d37b4383b..183660846 100644 --- a/pkg/rpcclient/actor/waiter.go +++ b/pkg/rpcclient/actor/waiter.go @@ -100,10 +100,13 @@ func errIsAlreadyExists(err error) bool { return strings.Contains(strings.ToLower(err.Error()), "already exists") } -// newWaiter creates Waiter instance. It can be either websocket-based or -// polling-base, otherwise Waiter stub is returned. -func newWaiter(ra RPCActor, v *result.Version) Waiter { - if eventW, ok := ra.(RPCEventWaiter); ok { +// NewWaiter creates Waiter instance. It can be either websocket-based or +// polling-base, otherwise Waiter stub is returned. As a first argument +// it accepts RPCEventWaiter implementation, RPCPollingWaiter implementation +// or not an implementation of these two interfaces. It returns websocket-based +// waiter, polling-based waiter or a stub correspondingly. +func NewWaiter(base any, v *result.Version) Waiter { + if eventW, ok := base.(RPCEventWaiter); ok { return &EventWaiter{ ws: eventW, polling: &PollingWaiter{ @@ -112,7 +115,7 @@ func newWaiter(ra RPCActor, v *result.Version) Waiter { }, } } - if pollW, ok := ra.(RPCPollingWaiter); ok { + if pollW, ok := base.(RPCPollingWaiter); ok { return &PollingWaiter{ polling: pollW, version: v, diff --git a/pkg/rpcclient/actor/waiter_test.go b/pkg/rpcclient/actor/waiter_test.go index f6b06fb00..495e62815 100644 --- a/pkg/rpcclient/actor/waiter_test.go +++ b/pkg/rpcclient/actor/waiter_test.go @@ -38,15 +38,15 @@ func (c *AwaitableRPCClient) ReceiveExecutions(flt *neorpc.ExecutionFilter, rcvr func (c *AwaitableRPCClient) Unsubscribe(id string) error { return nil } func TestNewWaiter(t *testing.T) { - w := newWaiter((RPCActor)(nil), nil) + w := NewWaiter((RPCActor)(nil), nil) _, ok := w.(NullWaiter) require.True(t, ok) - w = newWaiter(&RPCClient{}, &result.Version{}) + w = NewWaiter(&RPCClient{}, &result.Version{}) _, ok = w.(*PollingWaiter) require.True(t, ok) - w = newWaiter(&AwaitableRPCClient{RPCClient: RPCClient{}}, &result.Version{}) + w = NewWaiter(&AwaitableRPCClient{RPCClient: RPCClient{}}, &result.Version{}) _, ok = w.(*EventWaiter) require.True(t, ok) } @@ -58,7 +58,7 @@ func TestPollingWaiter_Wait(t *testing.T) { expected := &state.AppExecResult{Container: h, Execution: state.Execution{}} c := &RPCClient{appLog: appLog} c.bCount.Store(bCount) - w := newWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. + w := NewWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. _, ok := w.(*PollingWaiter) require.True(t, ok) @@ -123,7 +123,7 @@ func TestWSWaiter_Wait(t *testing.T) { expected := &state.AppExecResult{Container: h, Execution: state.Execution{}} c := &AwaitableRPCClient{RPCClient: RPCClient{appLog: appLog}} c.bCount.Store(bCount) - w := newWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. + w := NewWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. _, ok := w.(*EventWaiter) require.True(t, ok) From 4c6dca876cba754291dd00a7dbca58442fada5c3 Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Tue, 26 Dec 2023 13:04:45 +0300 Subject: [PATCH 2/4] rpcclient: move Waiter to a separate package There are use-cases when not only Actor, but also Invoker and even simple RPC client must wait (e.g. sendtx or dumptx CLI commands). Actor requires optional signers in constructor, and it's not always appropriate to create Actor only to be able to use Waiter, sometimes it's needed to use only Waiter without Actor. Signed-off-by: Ekaterina Pavlova --- pkg/rpcclient/actor/actor.go | 5 +- pkg/rpcclient/actor/compat_test.go | 6 -- pkg/rpcclient/notary/actor_test.go | 3 +- pkg/rpcclient/{actor => waiter}/waiter.go | 6 +- .../{actor => waiter}/waiter_test.go | 98 ++++++++++++++++--- 5 files changed, 91 insertions(+), 27 deletions(-) rename pkg/rpcclient/{actor => waiter}/waiter.go (98%) rename pkg/rpcclient/{actor => waiter}/waiter_test.go (55%) diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go index bf3ec334e..e81a581da 100644 --- a/pkg/rpcclient/actor/actor.go +++ b/pkg/rpcclient/actor/actor.go @@ -17,6 +17,7 @@ import ( "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/rpcclient/waiter" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" ) @@ -72,7 +73,7 @@ type SignerAccount struct { // and ErrTxNotAccepted is returned if transaction wasn't accepted by this moment. type Actor struct { invoker.Invoker - Waiter + waiter.Waiter client RPCActor opts Options @@ -126,7 +127,7 @@ func New(ra RPCActor, signers []SignerAccount) (*Actor, error) { } return &Actor{ Invoker: *inv, - Waiter: NewWaiter(ra, version), + Waiter: waiter.New(ra, version), client: ra, opts: NewDefaultOptions(), signers: signers, diff --git a/pkg/rpcclient/actor/compat_test.go b/pkg/rpcclient/actor/compat_test.go index b8e356898..874025d14 100644 --- a/pkg/rpcclient/actor/compat_test.go +++ b/pkg/rpcclient/actor/compat_test.go @@ -11,9 +11,3 @@ func TestRPCActorRPCClientCompat(t *testing.T) { _ = actor.RPCActor(&rpcclient.WSClient{}) _ = actor.RPCActor(&rpcclient.Client{}) } - -func TestRPCWaiterRPCClientCompat(t *testing.T) { - _ = actor.RPCPollingWaiter(&rpcclient.Client{}) - _ = actor.RPCPollingWaiter(&rpcclient.WSClient{}) - _ = actor.RPCEventWaiter(&rpcclient.WSClient{}) -} diff --git a/pkg/rpcclient/notary/actor_test.go b/pkg/rpcclient/notary/actor_test.go index 16fc7ce4c..ccda87c5b 100644 --- a/pkg/rpcclient/notary/actor_test.go +++ b/pkg/rpcclient/notary/actor_test.go @@ -15,6 +15,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/network/payload" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" @@ -78,7 +79,7 @@ func (r *RPCClient) GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*r return r.applog, nil } -var _ = actor.RPCPollingWaiter(&RPCClient{}) +var _ = waiter.RPCPollingWaiter(&RPCClient{}) func TestNewActor(t *testing.T) { rc := &RPCClient{ diff --git a/pkg/rpcclient/actor/waiter.go b/pkg/rpcclient/waiter/waiter.go similarity index 98% rename from pkg/rpcclient/actor/waiter.go rename to pkg/rpcclient/waiter/waiter.go index 183660846..dd17e2a19 100644 --- a/pkg/rpcclient/actor/waiter.go +++ b/pkg/rpcclient/waiter/waiter.go @@ -1,4 +1,4 @@ -package actor +package waiter import ( "context" @@ -100,12 +100,12 @@ func errIsAlreadyExists(err error) bool { return strings.Contains(strings.ToLower(err.Error()), "already exists") } -// NewWaiter creates Waiter instance. It can be either websocket-based or +// New creates Waiter instance. It can be either websocket-based or // polling-base, otherwise Waiter stub is returned. As a first argument // it accepts RPCEventWaiter implementation, RPCPollingWaiter implementation // or not an implementation of these two interfaces. It returns websocket-based // waiter, polling-based waiter or a stub correspondingly. -func NewWaiter(base any, v *result.Version) Waiter { +func New(base any, v *result.Version) Waiter { if eventW, ok := base.(RPCEventWaiter); ok { return &EventWaiter{ ws: eventW, diff --git a/pkg/rpcclient/actor/waiter_test.go b/pkg/rpcclient/waiter/waiter_test.go similarity index 55% rename from pkg/rpcclient/actor/waiter_test.go rename to pkg/rpcclient/waiter/waiter_test.go index 495e62815..1eaff101a 100644 --- a/pkg/rpcclient/actor/waiter_test.go +++ b/pkg/rpcclient/waiter/waiter_test.go @@ -1,20 +1,82 @@ -package actor +package waiter_test import ( "context" "errors" "sync" + "sync/atomic" "testing" "time" + "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/block" "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc" "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/rpcclient/waiter" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/stretchr/testify/require" ) +type RPCClient struct { + err error + invRes *result.Invoke + netFee int64 + bCount atomic.Uint32 + version *result.Version + hash util.Uint256 + appLog *result.ApplicationLog + context context.Context +} + +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.Load(), 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 (r *RPCClient) TerminateSession(sessionID uuid.UUID) (bool, error) { + return false, nil // Just a stub, unused by actor. +} +func (r *RPCClient) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) { + return nil, nil // Just a stub, unused by actor. +} +func (r *RPCClient) Context() context.Context { + if r.context == nil { + return context.Background() + } + return r.context +} + +func (r *RPCClient) GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*result.ApplicationLog, error) { + if r.appLog != nil { + return r.appLog, nil + } + return nil, errors.New("not found") +} + type AwaitableRPCClient struct { RPCClient @@ -38,16 +100,16 @@ func (c *AwaitableRPCClient) ReceiveExecutions(flt *neorpc.ExecutionFilter, rcvr func (c *AwaitableRPCClient) Unsubscribe(id string) error { return nil } func TestNewWaiter(t *testing.T) { - w := NewWaiter((RPCActor)(nil), nil) - _, ok := w.(NullWaiter) + w := waiter.New((actor.RPCActor)(nil), nil) + _, ok := w.(waiter.NullWaiter) require.True(t, ok) - w = NewWaiter(&RPCClient{}, &result.Version{}) - _, ok = w.(*PollingWaiter) + w = waiter.New(&RPCClient{}, &result.Version{}) + _, ok = w.(*waiter.PollingWaiter) require.True(t, ok) - w = NewWaiter(&AwaitableRPCClient{RPCClient: RPCClient{}}, &result.Version{}) - _, ok = w.(*EventWaiter) + w = waiter.New(&AwaitableRPCClient{RPCClient: RPCClient{}}, &result.Version{}) + _, ok = w.(*waiter.EventWaiter) require.True(t, ok) } @@ -58,8 +120,8 @@ func TestPollingWaiter_Wait(t *testing.T) { expected := &state.AppExecResult{Container: h, Execution: state.Execution{}} c := &RPCClient{appLog: appLog} c.bCount.Store(bCount) - w := NewWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. - _, ok := w.(*PollingWaiter) + w := waiter.New(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. + _, ok := w.(*waiter.PollingWaiter) require.True(t, ok) // Wait with error. @@ -75,7 +137,7 @@ func TestPollingWaiter_Wait(t *testing.T) { // Missing AER after VUB. c.appLog = nil _, err = w.Wait(h, bCount-2, nil) - require.ErrorIs(t, ErrTxNotAccepted, err) + require.ErrorIs(t, waiter.ErrTxNotAccepted, err) checkErr := func(t *testing.T, trigger func(), target error) { errCh := make(chan error) @@ -106,14 +168,14 @@ func TestPollingWaiter_Wait(t *testing.T) { // Tx is accepted before VUB. c.appLog = nil c.bCount.Store(bCount) - checkErr(t, func() { c.bCount.Store(bCount + 1) }, ErrTxNotAccepted) + checkErr(t, func() { c.bCount.Store(bCount + 1) }, waiter.ErrTxNotAccepted) // Context is cancelled. c.appLog = nil c.bCount.Store(bCount) ctx, cancel := context.WithCancel(context.Background()) c.context = ctx - checkErr(t, cancel, ErrContextDone) + checkErr(t, cancel, waiter.ErrContextDone) } func TestWSWaiter_Wait(t *testing.T) { @@ -123,8 +185,8 @@ func TestWSWaiter_Wait(t *testing.T) { expected := &state.AppExecResult{Container: h, Execution: state.Execution{}} c := &AwaitableRPCClient{RPCClient: RPCClient{appLog: appLog}} c.bCount.Store(bCount) - w := NewWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. - _, ok := w.(*EventWaiter) + w := waiter.New(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time. + _, ok := w.(*waiter.EventWaiter) require.True(t, ok) // Wait with error. @@ -176,7 +238,7 @@ func TestWSWaiter_Wait(t *testing.T) { // Missing AER after VUB. go func() { _, err = w.Wait(h, bCount-2, nil) - require.ErrorIs(t, err, ErrTxNotAccepted) + require.ErrorIs(t, err, waiter.ErrTxNotAccepted) doneCh <- struct{}{} }() check(t, func() { @@ -185,3 +247,9 @@ func TestWSWaiter_Wait(t *testing.T) { c.subBlockCh <- &block.Block{} }) } + +func TestRPCWaiterRPCClientCompat(t *testing.T) { + _ = waiter.RPCPollingWaiter(&rpcclient.Client{}) + _ = waiter.RPCPollingWaiter(&rpcclient.WSClient{}) + _ = waiter.RPCEventWaiter(&rpcclient.WSClient{}) +} From 4f5e3f363a83bbb2ca43471097cc1ff90deebbb2 Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Wed, 27 Dec 2023 12:26:02 +0300 Subject: [PATCH 3/4] cli: fix canceltx ValidUntilBlock parameter of conflicting transaction If main transaction is known, then conflicting transaction shouldn't be valid longer than the main one. Signed-off-by: Ekaterina Pavlova --- cli/util/cancel.go | 3 +++ cli/util/convert.go | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/cli/util/cancel.go b/cli/util/cancel.go index 495704e3e..19376217d 100644 --- a/cli/util/cancel.go +++ b/cli/util/cancel.go @@ -64,6 +64,9 @@ func cancelTx(ctx *cli.Context) error { t.NetworkFee = mainTx.NetworkFee + 1 } t.NetworkFee += int64(flags.Fixed8FromContext(ctx, "gas")) + if mainTx != nil { + t.ValidUntilBlock = mainTx.ValidUntilBlock + } return nil }) if err != nil { diff --git a/cli/util/convert.go b/cli/util/convert.go index 002711b52..98ea3ac22 100644 --- a/cli/util/convert.go +++ b/cli/util/convert.go @@ -56,16 +56,16 @@ func NewCommands() []cli.Command { Usage: "Cancel transaction by sending conflicting transaction", UsageText: "canceltx -r --wallet [--account ] [--wallet-config ] [--gas ]", Description: `Aims to prevent a transaction from being added to the blockchain by dispatching a more -prioritized conflicting transaction to the specified RPC node. The input for this command should -be the transaction hash. If another account is not specified, the conflicting transaction is -automatically generated and signed by the default account in the wallet. If the target transaction -is in the memory pool of the provided RPC node, the NetworkFee value of the conflicting transaction -is set to the target transaction's NetworkFee value plus one (if it's sufficient for the -conflicting transaction itself). If the target transaction is not in the memory pool, standard -NetworkFee calculations are performed based on the calculatenetworkfee RPC request. If the --gas -flag is included, the specified value is added to the resulting conflicting transaction network fee -in both scenarios. -`, + prioritized conflicting transaction to the specified RPC node. The input for this command should + be the transaction hash. If another account is not specified, the conflicting transaction is + automatically generated and signed by the default account in the wallet. If the target transaction + is in the memory pool of the provided RPC node, the NetworkFee value of the conflicting transaction + is set to the target transaction's NetworkFee value plus one (if it's sufficient for the + conflicting transaction itself), the ValidUntilBlock value of the conflicting transaction is set to the + target transaction's ValidUntilBlock value. If the target transaction is not in the memory pool, standard + NetworkFee calculations are performed based on the calculatenetworkfee RPC request. If the --gas + flag is included, the specified value is added to the resulting conflicting transaction network fee + in both scenarios.`, Action: cancelTx, Flags: txCancelFlags, }, From 0ffa24932b2ff85dac3b27ffc30fe1c292215db6 Mon Sep 17 00:00:00 2001 From: Ekaterina Pavlova Date: Thu, 28 Dec 2023 14:58:38 +0300 Subject: [PATCH 4/4] cli: add await flag for operations with transactions New --await flag is an option to synchronize on transaction execution for CLI commands. Closes #3244 Signed-off-by: Ekaterina Pavlova --- cli/nep_test/nep11_test.go | 8 +++ cli/nep_test/nep17_test.go | 13 ++++- cli/options/options.go | 13 ++++- cli/smartcontract/contract_test.go | 27 +++++++-- cli/smartcontract/smart_contract.go | 12 ++-- cli/txctx/tx.go | 38 +++++++++++- cli/util/cancel.go | 31 +++++++++- cli/util/convert.go | 19 ++++-- cli/util/send.go | 13 ++++- cli/util/util_test.go | 41 +++++++++++++ cli/wallet/candidate_test.go | 57 ++++++++++++++++++ cli/wallet/multisig.go | 15 ++++- cli/wallet/nep11.go | 5 +- cli/wallet/nep17.go | 10 +++- cli/wallet/validator.go | 12 ++-- cli/wallet/wallet.go | 10 +++- cli/wallet/wallet_test.go | 91 +++++++++++++++++++++++++++++ internal/testcli/executor.go | 7 +++ 18 files changed, 383 insertions(+), 39 deletions(-) diff --git a/cli/nep_test/nep11_test.go b/cli/nep_test/nep11_test.go index 334a27dd7..e2866c107 100644 --- a/cli/nep_test/nep11_test.go +++ b/cli/nep_test/nep11_test.go @@ -328,6 +328,14 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) { e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...) checkBalanceResult(t, nftOwnerAddr, tokenID1) + // check --await flag + tokenID2 := mint(t) + e.In.WriteString(nftOwnerPass + "\r") + e.Run(t, append(cmdTransfer, "--await", "--id", hex.EncodeToString(tokenID2))...) + e.CheckAwaitableTxPersisted(t) + e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...) + checkBalanceResult(t, nftOwnerAddr, tokenID1) + // transfer: good, to NEP-11-Payable contract, with data verifyH := deployVerifyContract(t, e) cmdTransfer = []string{ diff --git a/cli/nep_test/nep17_test.go b/cli/nep_test/nep17_test.go index 5964added..f933e0559 100644 --- a/cli/nep_test/nep17_test.go +++ b/cli/nep_test/nep17_test.go @@ -223,12 +223,19 @@ func TestNEP17Transfer(t *testing.T) { "neo-go", "wallet", "nep17", "transfer", "--rpc-endpoint", "http://" + e.RPC.Addresses()[0], "--wallet", testcli.ValidatorWallet, - "--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()), "--token", "GAS", "--amount", "1", - "--from", testcli.ValidatorAddr, "--force", - "[", testcli.ValidatorAddr, strconv.Itoa(int(validTil)), "]"} + "--from", testcli.ValidatorAddr} + + t.Run("with await", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, "--to", nftOwnerAddr, "--await")...) + e.CheckAwaitableTxPersisted(t) + }) + + cmd = append(cmd, "--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()), + "[", testcli.ValidatorAddr, strconv.Itoa(int(validTil)), "]") t.Run("with data", func(t *testing.T) { e.In.WriteString("one\r") diff --git a/cli/options/options.go b/cli/options/options.go index 1cef56915..5fdc59c2f 100644 --- a/cli/options/options.go +++ b/cli/options/options.go @@ -32,8 +32,14 @@ import ( "gopkg.in/yaml.v3" ) -// DefaultTimeout is the default timeout used for RPC requests. -const DefaultTimeout = 10 * time.Second +const ( + // DefaultTimeout is the default timeout used for RPC requests. + DefaultTimeout = 10 * time.Second + // DefaultAwaitableTimeout is the default timeout used for RPC requests that + // require transaction awaiting. It is set to the approximate time of three + // Neo N3 mainnet blocks accepting. + DefaultAwaitableTimeout = 3 * 15 * time.Second +) // RPCEndpointFlag is a long flag name for an RPC endpoint. It can be used to // check for flag presence in the context. @@ -129,6 +135,9 @@ func GetTimeoutContext(ctx *cli.Context) (context.Context, func()) { if dur == 0 { dur = DefaultTimeout } + if !ctx.IsSet("timeout") && ctx.Bool("await") { + dur = DefaultAwaitableTimeout + } return context.WithTimeout(context.Background(), dur) } diff --git a/cli/smartcontract/contract_test.go b/cli/smartcontract/contract_test.go index a263413cc..4051952e0 100644 --- a/cli/smartcontract/contract_test.go +++ b/cli/smartcontract/contract_test.go @@ -338,7 +338,7 @@ func TestContractDeployWithData(t *testing.T) { "--config", "testdata/deploy/neo-go.yml", "--out", nefName, "--manifest", manifestName) - deployContract := func(t *testing.T, haveData bool, scope string) { + deployContract := func(t *testing.T, haveData bool, scope string, await bool) { e := testcli.NewExecutor(t, true) cmd := []string{ "neo-go", "contract", "deploy", @@ -348,6 +348,9 @@ func TestContractDeployWithData(t *testing.T) { "--force", } + if await { + cmd = append(cmd, "--await") + } if haveData { cmd = append(cmd, "[", "key1", "12", "key2", "take_me_to_church", "]") } @@ -358,8 +361,13 @@ func TestContractDeployWithData(t *testing.T) { } e.In.WriteString(testcli.ValidatorPass + "\r") e.Run(t, cmd...) + var tx *transaction.Transaction + if await { + tx, _ = e.CheckAwaitableTxPersisted(t) + } else { + tx, _ = e.CheckTxPersisted(t) + } - tx, _ := e.CheckTxPersisted(t, "Sent invocation transaction ") require.Equal(t, scope, tx.Signers[0].Scopes.String()) if !haveData { return @@ -396,9 +404,12 @@ func TestContractDeployWithData(t *testing.T) { require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value()) } - deployContract(t, true, "") - deployContract(t, false, "Global") - deployContract(t, true, "Global") + deployContract(t, true, "", false) + deployContract(t, false, "Global", false) + deployContract(t, true, "Global", false) + deployContract(t, false, "", true) + deployContract(t, true, "Global", true) + deployContract(t, true, "", true) } func TestDeployWithSigners(t *testing.T) { @@ -772,6 +783,12 @@ func TestComlileAndInvokeFunction(t *testing.T) { e.Run(t, append(cmd, h.StringLE(), "getValue", "--", testcli.ValidatorAddr, hVerify.StringLE())...) }) + + t.Run("with await", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, "--force", "--await", h.StringLE(), "getValue")...) + e.CheckAwaitableTxPersisted(t) + }) }) t.Run("real invoke and save tx", func(t *testing.T) { diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index ec3fe68fc..00c0cce89 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -91,6 +91,7 @@ func NewCommands() []cli.Command { txctx.SysGasFlag, txctx.OutFlag, txctx.ForceFlag, + txctx.AwaitFlag, } invokeFunctionFlags = append(invokeFunctionFlags, options.Wallet...) invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...) @@ -188,10 +189,12 @@ func NewCommands() []cli.Command { { Name: "deploy", Usage: "deploy a smart contract (.nef with description)", - UsageText: "neo-go contract deploy -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] --in contract.nef --manifest contract.manifest.json [--out file] [--force] [data]", + UsageText: "neo-go contract deploy -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] --in contract.nef --manifest contract.manifest.json [--out file] [--force] [--await] [data]", Description: `Deploys given contract into the chain. The gas parameter is for additional gas to be added as a network fee to prioritize the transaction. The data - parameter is an optional parameter to be passed to '_deploy' method. + parameter is an optional parameter to be passed to '_deploy' method. When + --await flag is specified, it waits for the transaction to be included + in a block. `, Action: contractDeploy, Flags: deployFlags, @@ -201,13 +204,14 @@ func NewCommands() []cli.Command { { Name: "invokefunction", Usage: "invoke deployed contract on the blockchain", - UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] [--out file] [--force] scripthash [method] [arguments...] [--] [signers...]", + UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] [--out file] [--force] [--await] scripthash [method] [arguments...] [--] [signers...]", Description: `Executes given (as a script hash) deployed script with the given method, arguments and signers. Sender is included in the list of signers by default with None witness scope. If you'd like to change default sender's scope, specify it via signers parameter. See testinvokefunction documentation for the details about parameters. It differs from testinvokefunction in that this - command sends an invocation transaction to the network. + command sends an invocation transaction to the network. When --await flag is + specified, it waits for the transaction to be included in a block. `, Action: invokeFunction, Flags: invokeFunctionFlags, diff --git a/cli/txctx/tx.go b/cli/txctx/tx.go index 64a6b0f67..c7cee5c07 100644 --- a/cli/txctx/tx.go +++ b/cli/txctx/tx.go @@ -5,13 +5,16 @@ package txctx import ( "fmt" + "io" "time" "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/paramcontext" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" + "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" ) @@ -37,6 +40,11 @@ var ( Name: "force", Usage: "Do not ask for a confirmation (and ignore errors)", } + // AwaitFlag is a flag used to wait for the transaction to be included in a block. + AwaitFlag = cli.BoolFlag{ + Name: "await", + Usage: "wait for the transaction to be included in a block", + } ) // SignAndSend adds network and system fees to the provided transaction and @@ -48,6 +56,7 @@ func SignAndSend(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, tx *tr gas = flags.Fixed8FromContext(ctx, "gas") sysgas = flags.Fixed8FromContext(ctx, "sysgas") ver = act.GetVersion() + aer *state.AppExecResult ) tx.SystemFee += int64(sysgas) @@ -68,12 +77,37 @@ func SignAndSend(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, tx *tr // Compensate for confirmation waiting. tx.ValidUntilBlock += uint32((waitTime.Milliseconds() / int64(ver.Protocol.MillisecondsPerBlock))) + 1 } - _, _, err = act.SignAndSend(tx) + var ( + resTx util.Uint256 + vub uint32 + ) + resTx, vub, err = act.SignAndSend(tx) + if err != nil { + return cli.NewExitError(err, 1) + } + if ctx.Bool("await") { + aer, err = act.Wait(resTx, vub, err) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to await transaction %s: %w", resTx.StringLE(), err), 1) + } + } } if err != nil { return cli.NewExitError(err, 1) } - fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) + DumpTransactionInfo(ctx.App.Writer, tx.Hash(), aer) return nil } + +// DumpTransactionInfo prints transaction info to the given writer. +func DumpTransactionInfo(w io.Writer, h util.Uint256, res *state.AppExecResult) { + fmt.Fprintln(w, h.StringLE()) + if res != nil { + fmt.Fprintf(w, "OnChain:\t%t\n", res != nil) + fmt.Fprintf(w, "VMState:\t%s\n", res.VMState.String()) + if res.FaultException != "" { + fmt.Fprintf(w, "FaultException:\t%s\n", res.FaultException) + } + } +} diff --git a/cli/util/cancel.go b/cli/util/cancel.go index 19376217d..531b941b6 100644 --- a/cli/util/cancel.go +++ b/cli/util/cancel.go @@ -1,15 +1,19 @@ package util import ( + "errors" "fmt" "strings" "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/options" + "github.com/nspcc-dev/neo-go/cli/txctx" + "github.com/nspcc-dev/neo-go/pkg/core/state" "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/actor" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/urfave/cli" @@ -55,7 +59,7 @@ func cancelTx(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("account %s is not a signer of the conflicting transaction", acc.Address), 1) } - resHash, _, err := a.SendTunedRun([]byte{byte(opcode.RET)}, []transaction.Attribute{{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: txHash}}}, func(r *result.Invoke, t *transaction.Transaction) error { + resHash, resVub, err := a.SendTunedRun([]byte{byte(opcode.RET)}, []transaction.Attribute{{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: txHash}}}, func(r *result.Invoke, t *transaction.Transaction) error { err := actor.DefaultCheckerModifier(r, t) if err != nil { return err @@ -72,6 +76,29 @@ func cancelTx(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to send conflicting transaction: %w", err), 1) } - fmt.Fprintln(ctx.App.Writer, resHash.StringLE()) + var ( + acceptedH = resHash + res *state.AppExecResult + ) + if ctx.Bool("await") { + res, err = a.WaitAny(gctx, resVub, txHash, resHash) + if err != nil { + if errors.Is(err, waiter.ErrTxNotAccepted) { + if mainTx == nil { + return cli.NewExitError(fmt.Errorf("neither target nor conflicting transaction is accepted before the current height %d (ValidUntilBlock value of conlicting transaction). Main transaction is unknown to the provided RPC node, thus still has chances to be accepted, you may try cancellation again", resVub), 1) + } + fmt.Fprintf(ctx.App.Writer, "Neither target nor conflicting transaction is accepted before the current height %d (ValidUntilBlock value of both target and conflicting transactions). Main transaction is not valid anymore, cancellation is successful\n", resVub) + return nil + } + return cli.NewExitError(fmt.Errorf("failed to await target/ conflicting transaction %s/ %s: %w", txHash.StringLE(), resHash.StringLE(), err), 1) + } + if txHash.Equals(res.Container) { + fmt.Fprintln(ctx.App.Writer, "Target transaction accepted") + acceptedH = txHash + } else { + fmt.Fprintln(ctx.App.Writer, "Conflicting transaction accepted") + } + } + txctx.DumpTransactionInfo(ctx.App.Writer, acceptedH, res) return nil } diff --git a/cli/util/convert.go b/cli/util/convert.go index 98ea3ac22..5d8090fef 100644 --- a/cli/util/convert.go +++ b/cli/util/convert.go @@ -17,12 +17,14 @@ import ( // NewCommands returns util commands for neo-go CLI. func NewCommands() []cli.Command { txDumpFlags := append([]cli.Flag{}, options.RPC...) + txSendFlags := append(txDumpFlags, txctx.AwaitFlag) txCancelFlags := append([]cli.Flag{ flags.AddressFlag{ Name: "address, a", Usage: "address to use as conflicting transaction signee (and gas source)", }, txctx.GasFlag, + txctx.AwaitFlag, }, options.RPC...) txCancelFlags = append(txCancelFlags, options.Wallet...) return []cli.Command{ @@ -42,19 +44,20 @@ func NewCommands() []cli.Command { { Name: "sendtx", Usage: "Send complete transaction stored in a context file", - UsageText: "sendtx [-r ] ", + UsageText: "sendtx [-r ] [--await]", Description: `Sends the transaction from the given context file to the given RPC node if it's completely signed and ready. This command expects a ContractParametersContext JSON file for input, it can't handle binary (or hex- or base64-encoded) - transactions. + transactions. If the --await flag is included, the command waits for the + transaction to be included in a block before exiting. `, Action: sendTx, - Flags: txDumpFlags, + Flags: txSendFlags, }, { Name: "canceltx", Usage: "Cancel transaction by sending conflicting transaction", - UsageText: "canceltx -r --wallet [--account ] [--wallet-config ] [--gas ]", + UsageText: "canceltx -r --wallet [--account ] [--wallet-config ] [--gas ] [--await]", Description: `Aims to prevent a transaction from being added to the blockchain by dispatching a more prioritized conflicting transaction to the specified RPC node. The input for this command should be the transaction hash. If another account is not specified, the conflicting transaction is @@ -65,7 +68,8 @@ func NewCommands() []cli.Command { target transaction's ValidUntilBlock value. If the target transaction is not in the memory pool, standard NetworkFee calculations are performed based on the calculatenetworkfee RPC request. If the --gas flag is included, the specified value is added to the resulting conflicting transaction network fee - in both scenarios.`, + in both scenarios. When the --await flag is included, the command waits for one of the conflicting + or target transactions to be included in a block.`, Action: cancelTx, Flags: txCancelFlags, }, @@ -75,6 +79,11 @@ func NewCommands() []cli.Command { UsageText: "txdump [-r ] ", Action: txDump, Flags: txDumpFlags, + Description: `Dumps the transaction from the given parameter context file to + the output. This command expects a ContractParametersContext JSON file for input, it can't handle + binary (or hex- or base64-encoded) transactions. If --rpc-endpoint flag is specified the result + of the given script after running it true the VM will be printed. Otherwise only transaction will + be printed.`, }, { Name: "ops", diff --git a/cli/util/send.go b/cli/util/send.go index 59814c103..a67465274 100644 --- a/cli/util/send.go +++ b/cli/util/send.go @@ -5,6 +5,9 @@ import ( "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/paramcontext" + "github.com/nspcc-dev/neo-go/cli/txctx" + "github.com/nspcc-dev/neo-go/pkg/core/state" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" "github.com/urfave/cli" ) @@ -37,6 +40,14 @@ func sendTx(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1) } - fmt.Fprintln(ctx.App.Writer, res.StringLE()) + var aer *state.AppExecResult + if ctx.Bool("await") { + version, err := c.GetVersion() + aer, err = waiter.New(c, version).Wait(res, tx.ValidUntilBlock, err) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to await transaction %s: %w", res.StringLE(), err), 1) + } + } + txctx.DumpTransactionInfo(ctx.App.Writer, res, aer) return nil } diff --git a/cli/util/util_test.go b/cli/util/util_test.go index 33b3ade74..64364bc25 100644 --- a/cli/util/util_test.go +++ b/cli/util/util_test.go @@ -136,3 +136,44 @@ func TestUtilCancelTx(t *testing.T) { return aerErr == nil }, time.Second*2, time.Millisecond*50) } + +func TestAwaitUtilCancelTx(t *testing.T) { + e := testcli.NewExecutor(t, true) + + w, err := wallet.NewWalletFromFile("../testdata/testwallet.json") + require.NoError(t, err) + + transferArgs := []string{ + "neo-go", "wallet", "nep17", "transfer", + "--rpc-endpoint", "http://" + e.RPC.Addresses()[0], + "--wallet", testcli.ValidatorWallet, + "--to", w.Accounts[0].Address, + "--token", "NEO", + "--from", testcli.ValidatorAddr, + "--force", + } + args := []string{"neo-go", "util", "canceltx", + "-r", "http://" + e.RPC.Addresses()[0], + "--wallet", testcli.ValidatorWallet, + "--address", testcli.ValidatorAddr, + "--await"} + + e.In.WriteString("one\r") + e.Run(t, append(transferArgs, "--amount", "1")...) + line := e.GetNextLine(t) + txHash, err := util.Uint256DecodeStringLE(line) + require.NoError(t, err) + + _, ok := e.Chain.GetMemPool().TryGetValue(txHash) + require.True(t, ok) + + e.In.WriteString("one\r") + e.Run(t, append(args, txHash.StringLE())...) + e.CheckNextLine(t, "Conflicting transaction accepted") + resHash, _ := e.CheckAwaitableTxPersisted(t) + + require.Eventually(t, func() bool { + _, aerErr := e.Chain.GetAppExecResults(resHash.Hash(), trigger.Application) + return aerErr == nil + }, time.Second*2, time.Millisecond*50) +} diff --git a/cli/wallet/candidate_test.go b/cli/wallet/candidate_test.go index 446b9a82f..6a23ab020 100644 --- a/cli/wallet/candidate_test.go +++ b/cli/wallet/candidate_test.go @@ -159,4 +159,61 @@ func TestRegisterCandidate(t *testing.T) { e.RunWithError(t, "neo-go", "query", "voter", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], validatorAddress, validatorAddress) e.RunWithError(t, "neo-go", "query", "committee", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], "something") e.RunWithError(t, "neo-go", "query", "candidates", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], "something") + + t.Run("VoteUnvote await", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "candidate", "register", + "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], + "--wallet", testcli.ValidatorWallet, + "--address", validatorAddress, + "--force", "--await") + e.CheckAwaitableTxPersisted(t) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "candidate", "vote", + "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], + "--wallet", testcli.ValidatorWallet, + "--address", validatorAddress, + "--candidate", validatorHex, + "--force", + "--await") + + e.CheckAwaitableTxPersisted(t) + b, _ := e.Chain.GetGoverningTokenBalance(testcli.ValidatorPriv.GetScriptHash()) + + // unvote + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "candidate", "vote", + "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], + "--wallet", testcli.ValidatorWallet, + "--address", validatorAddress, + "--force", "--await") + _, index := e.CheckAwaitableTxPersisted(t) + + vs, err = e.Chain.GetEnrollments() + require.Equal(t, 1, len(vs)) + require.Equal(t, validatorPublic, vs[0].Key) + require.Equal(t, big.NewInt(0), vs[0].Votes) + + // check state + e.Run(t, "neo-go", "query", "voter", + "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], + validatorAddress) + e.CheckNextLine(t, "^\\s*Voted:\\s+"+"null") // no vote. + e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+b.String()+"$") + e.CheckNextLine(t, "^\\s*Block\\s*:\\s*"+strconv.FormatUint(uint64(index), 10)) + e.CheckEOF(t) + }) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "wallet", "candidate", "unregister", + "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], + "--wallet", testcli.ValidatorWallet, + "--address", validatorAddress, + "--force", + "--await") + e.CheckAwaitableTxPersisted(t) + + vs, err = e.Chain.GetEnrollments() + require.Equal(t, 0, len(vs)) } diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go index 337064e77..f6d600da9 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -8,7 +8,10 @@ import ( "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/paramcontext" + "github.com/nspcc-dev/neo-go/cli/txctx" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter" "github.com/urfave/cli" ) @@ -17,6 +20,7 @@ func signStoredTransaction(ctx *cli.Context) error { out = ctx.String("out") rpcNode = ctx.String(options.RPCEndpointFlag) addrFlag = ctx.Generic("address").(*flags.Address) + aer *state.AppExecResult ) if err := cmdargs.EnsureNone(ctx); err != nil { return err @@ -84,10 +88,15 @@ func signStoredTransaction(ctx *cli.Context) error { if err != nil { return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1) } - fmt.Fprintln(ctx.App.Writer, res.StringLE()) - return nil + if ctx.Bool("await") { + version, err := c.GetVersion() + aer, err = waiter.New(c, version).Wait(res, tx.ValidUntilBlock, err) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to await transaction %s: %w", res.StringLE(), err), 1) + } + } } - fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) + txctx.DumpTransactionInfo(ctx.App.Writer, tx.Hash(), aer) return nil } diff --git a/cli/wallet/nep11.go b/cli/wallet/nep11.go index 8c5b9caf6..9fcce1a86 100644 --- a/cli/wallet/nep11.go +++ b/cli/wallet/nep11.go @@ -101,7 +101,7 @@ func newNEP11Commands() []cli.Command { { Name: "transfer", Usage: "transfer NEP-11 tokens", - UsageText: "transfer -w wallet [--wallet-config path] --rpc-endpoint --timeout