Repository: nspcc-dev/neo-go
Pull request #3491 (2024-06-20 14:01:28)

actor: add a new WaitSuccess API
Most of the time people are interested in successful executions. Unfortunately, unwrap package can't help here because of a different result structure (some interface abstract can help, but it's still mostly stack-oriented and sessions can be a problem), so this additional interface is needed.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/nspcc-dev/neo-go/pull/3491?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 86.13%. Comparing base [(`4ff2063`)](https://app.codecov.io/gh/nspcc-dev/neo-go/commit/4ff2063539430d18f499fea32c347f6396fadf38?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev) to head [(`b713e70`)](https://app.codecov.io/gh/nspcc-dev/neo-go/commit/b713e701e03aa060f260df8d3c13cfa3291f3835?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #3491      +/-   ##
==========================================
- Coverage   86.19%   86.13%   -0.07%     
==========================================
  Files         331      331              
  Lines       38494    38510      +16     
==========================================
- Hits        33179    33169      -10     
- Misses       3785     3811      +26     
  Partials     1530     1530              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/nspcc-dev/neo-go/pull/3491?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=nspcc-dev).


Patch:
diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go
index 2884c3cd2..3a884e80b 100644
--- a/pkg/rpcclient/actor/actor.go
+++ b/pkg/rpcclient/actor/actor.go
@@ -14,14 +14,23 @@ import (
 	"fmt"
 
 	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
+	"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/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/vm/vmstate"
 	"github.com/nspcc-dev/neo-go/pkg/wallet"
 )
 
+var (
+	// ErrExecFailed is returned from [Actor.WaitSuccess] when transaction
+	// is accepted into a block, but its execution ended up in non-HALT VM
+	// state.
+	ErrExecFailed = errors.New("execution failed")
+)
+
 // RPCActor is an interface required from the RPC client to successfully
 // create and send transactions.
 type RPCActor interface {
@@ -285,3 +294,17 @@ func (a *Actor) SendUncheckedRun(script []byte, sysfee int64, attrs []transactio
 func (a *Actor) Sender() util.Uint160 {
 	return a.txSigners[0].Account
 }
+
+// WaitSuccess is similar to [waiter.Wait], but also checks for the VM state
+// to be HALT (successful execution). Execution result is still returned (if
+// HALTed normally) in case you need to examine events or stack.
+func (a *Actor) WaitSuccess(h util.Uint256, vub uint32, err error) (*state.AppExecResult, error) {
+	aer, err := a.Wait(h, vub, err)
+	if err != nil {
+		return nil, err
+	}
+	if aer.VMState != vmstate.Halt {
+		return nil, fmt.Errorf("%w: %s", ErrExecFailed, aer.FaultException)
+	}
+	return aer, nil
+}
diff --git a/pkg/rpcclient/notary/actor.go b/pkg/rpcclient/notary/actor.go
index 94cbaae8b..c65f06030 100644
--- a/pkg/rpcclient/notary/actor.go
+++ b/pkg/rpcclient/notary/actor.go
@@ -16,9 +16,16 @@ import (
 	"github.com/nspcc-dev/neo-go/pkg/util"
 	"github.com/nspcc-dev/neo-go/pkg/vm"
 	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
+	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
 	"github.com/nspcc-dev/neo-go/pkg/wallet"
 )
 
+var (
+	// ErrFallbackAccepted is returned from [Actor.WaitSuccess] when
+	// fallback transaction enters the chain instead of the main one.
+	ErrFallbackAccepted = errors.New("fallback transaction accepted")
+)
+
 // Actor encapsulates everything needed to create proper notary requests for
 // assisted transactions.
 type Actor struct {
@@ -332,3 +339,21 @@ func (a *Actor) Wait(mainHash, fbHash util.Uint256, vub uint32, err error) (*sta
 	}
 	return a.WaitAny(context.TODO(), vub, mainHash, fbHash)
 }
+
+// WaitSuccess works similar to [Actor.Wait], but checks that the main
+// transaction was accepted and it has a HALT VM state (executed successfully).
+// [state.AppExecResult] is still returned (if there is no error) in case you
+// need some additional event or stack checks.
+func (a *Actor) WaitSuccess(mainHash, fbHash util.Uint256, vub uint32, err error) (*state.AppExecResult, error) {
+	aer, err := a.Wait(mainHash, fbHash, vub, err)
+	if err != nil {
+		return nil, err
+	}
+	if aer.Container != mainHash {
+		return nil, ErrFallbackAccepted
+	}
+	if aer.VMState != vmstate.Halt {
+		return nil, fmt.Errorf("%w: %s", actor.ErrExecFailed, aer.FaultException)
+	}
+	return aer, nil
+}


Test patch:
diff --git a/pkg/rpcclient/actor/actor_test.go b/pkg/rpcclient/actor/actor_test.go
index 4bd1b98a8..7b2471056 100644
--- a/pkg/rpcclient/actor/actor_test.go
+++ b/pkg/rpcclient/actor/actor_test.go
@@ -8,12 +8,14 @@ import (
 
 	"github.com/google/uuid"
 	"github.com/nspcc-dev/neo-go/pkg/config/netmode"
+	"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/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/nspcc-dev/neo-go/pkg/vm/vmstate"
 	"github.com/nspcc-dev/neo-go/pkg/wallet"
 	"github.com/stretchr/testify/require"
 )
@@ -295,3 +297,38 @@ func TestSender(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, acc.ScriptHash(), a.Sender())
 }
+
+func TestWaitSuccess(t *testing.T) {
+	client, acc := testRPCAndAccount(t)
+	a, err := NewSimple(client, acc)
+	require.NoError(t, err)
+
+	someErr := errors.New("someErr")
+	_, err = a.WaitSuccess(util.Uint256{}, 0, someErr)
+	require.ErrorIs(t, err, someErr)
+
+	cont := util.Uint256{1, 2, 3}
+	ex := state.Execution{
+		Trigger:     trigger.Application,
+		VMState:     vmstate.Halt,
+		GasConsumed: 123,
+		Stack:       []stackitem.Item{stackitem.Null{}},
+	}
+	applog := &result.ApplicationLog{
+		Container:     cont,
+		IsTransaction: true,
+		Executions:    []state.Execution{ex},
+	}
+	client.appLog = applog
+	client.appLog.Executions[0].VMState = vmstate.Fault
+	_, err = a.WaitSuccess(util.Uint256{}, 0, nil)
+	require.ErrorIs(t, err, ErrExecFailed)
+
+	client.appLog.Executions[0].VMState = vmstate.Halt
+	res, err := a.WaitSuccess(util.Uint256{}, 0, nil)
+	require.NoError(t, err)
+	require.Equal(t, &state.AppExecResult{
+		Container: cont,
+		Execution: ex,
+	}, res)
+}
diff --git a/pkg/rpcclient/notary/actor_test.go b/pkg/rpcclient/notary/actor_test.go
index 57932e7e0..994590be8 100644
--- a/pkg/rpcclient/notary/actor_test.go
+++ b/pkg/rpcclient/notary/actor_test.go
@@ -565,6 +565,9 @@ func TestWait(t *testing.T) {
 	_, err = act.Wait(util.Uint256{}, util.Uint256{}, 0, someErr)
 	require.ErrorIs(t, err, someErr)
 
+	_, err = act.WaitSuccess(util.Uint256{}, util.Uint256{}, 0, someErr)
+	require.ErrorIs(t, err, someErr)
+
 	cont := util.Uint256{1, 2, 3}
 	ex := state.Execution{
 		Trigger:     trigger.Application,
@@ -584,4 +587,22 @@ func TestWait(t *testing.T) {
 		Container: cont,
 		Execution: ex,
 	}, res)
+
+	// Not successful since result has a different hash.
+	_, err = act.WaitSuccess(util.Uint256{}, util.Uint256{}, 0, nil)
+	require.ErrorIs(t, err, ErrFallbackAccepted)
+	_, err = act.WaitSuccess(util.Uint256{}, util.Uint256{1, 2, 3}, 0, nil)
+	require.ErrorIs(t, err, ErrFallbackAccepted)
+
+	rc.applog.Executions[0].VMState = vmstate.Fault
+	_, err = act.WaitSuccess(util.Uint256{1, 2, 3}, util.Uint256{}, 0, nil)
+	require.ErrorIs(t, err, actor.ErrExecFailed)
+
+	rc.applog.Executions[0].VMState = vmstate.Halt
+	res, err = act.WaitSuccess(util.Uint256{1, 2, 3}, util.Uint256{}, 0, nil)
+	require.NoError(t, err)
+	require.Equal(t, &state.AppExecResult{
+		Container: cont,
+		Execution: ex,
+	}, res)
 }