diff --git a/pkg/rpcclient/unwrap/unwrap.go b/pkg/rpcclient/unwrap/unwrap.go index 1e2d7286f..69315101b 100644 --- a/pkg/rpcclient/unwrap/unwrap.go +++ b/pkg/rpcclient/unwrap/unwrap.go @@ -25,12 +25,24 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/vmstate" ) +// Exception is a type used for VM fault messages (aka exceptions). If any of +// unwrapper functions encounters a FAULT VM state it creates an instance of +// this type as an error using exception string. It can be used with [errors.As] +// to get the exact message from VM and compare with known contract-specific +// errors. +type Exception string + // ErrNoSessionID is returned from the SessionIterator when the server does not // have sessions enabled and does not perform automatic iterator expansion. It // means you have no way to get the data from returned iterators using this // server, other than expanding it in the VM script. var ErrNoSessionID = errors.New("server returned iterator ID, but no session ID") +// Error implements the error interface. +func (e Exception) Error() string { + return string(e) +} + // BigInt expects correct execution (HALT state) with a single stack item // returned. A big.Int is extracted from this item and returned. func BigInt(r *result.Invoke, err error) (*big.Int, error) { @@ -399,10 +411,10 @@ func checkResOK(r *result.Invoke, err error) error { return err } if r.State != vmstate.Halt.String() { - return fmt.Errorf("invocation failed: %s", r.FaultException) + return fmt.Errorf("invocation failed: %w", Exception(r.FaultException)) } if r.FaultException != "" { - return fmt.Errorf("inconsistent result, HALTed with exception: %s", r.FaultException) + return fmt.Errorf("inconsistent result, HALTed with exception: %w", Exception(r.FaultException)) } return nil } diff --git a/pkg/rpcclient/unwrap/unwrap_test.go b/pkg/rpcclient/unwrap/unwrap_test.go index 51d1f451a..9dc137316 100644 --- a/pkg/rpcclient/unwrap/unwrap_test.go +++ b/pkg/rpcclient/unwrap/unwrap_test.go @@ -95,6 +95,20 @@ func TestStdErrors(t *testing.T) { for _, f := range funcs { _, err := f(&result.Invoke{State: "FAULT", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) require.Error(t, err) + + var fault Exception + require.True(t, errors.As(err, &fault)) + require.Equal(t, "", string(fault)) + } + }) + t.Run("FAULT state with exception", func(t *testing.T) { + for _, f := range funcs { + _, err := f(&result.Invoke{State: "FAULT", FaultException: "something bad", Stack: []stackitem.Item{stackitem.Make(42)}}, nil) + require.Error(t, err) + + var fault Exception + require.True(t, errors.As(err, &fault)) + require.Equal(t, "something bad", string(fault)) } }) t.Run("nothing returned", func(t *testing.T) { @@ -207,10 +221,12 @@ func TestItemJSONError(t *testing.T) { var received result.Invoke require.NoError(t, json.Unmarshal(data, &received)) + require.True(t, len(received.FaultException) != 0) _, err = Item(&received, nil) - require.True(t, len(received.FaultException) != 0) - require.Contains(t, err.Error(), received.FaultException) + var fault Exception + require.True(t, errors.As(err, &fault)) + require.Equal(t, received.FaultException, string(fault)) } func TestUTF8String(t *testing.T) {