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.

Signed-off-by: Roman Khimov <roman@nspcc.ru>
This commit is contained in:
Roman Khimov 2024-06-20 16:57:16 +03:00
parent 4ff2063539
commit a327a82085
4 changed files with 106 additions and 0 deletions

View file

@ -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
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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)
}