transaction: implement OracleResponse attribute
This commit is contained in:
parent
06b29e409c
commit
1625689316
8 changed files with 267 additions and 20 deletions
|
@ -10,7 +10,15 @@ import (
|
|||
|
||||
// Attribute represents a Transaction attribute.
|
||||
type Attribute struct {
|
||||
Type AttrType
|
||||
Type AttrType
|
||||
Value interface {
|
||||
io.Serializable
|
||||
// toJSONMap is used for embedded json struct marshalling.
|
||||
// Anonymous interface fields are not considered anonymous by
|
||||
// json lib and marshaling Value together with type makes code
|
||||
// harder to follow.
|
||||
toJSONMap(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
// attrJSON is used for JSON I/O of Attribute.
|
||||
|
@ -24,6 +32,9 @@ func (attr *Attribute) DecodeBinary(br *io.BinReader) {
|
|||
|
||||
switch attr.Type {
|
||||
case HighPriority:
|
||||
case OracleResponseT:
|
||||
attr.Value = new(OracleResponse)
|
||||
attr.Value.DecodeBinary(br)
|
||||
default:
|
||||
br.Err = fmt.Errorf("failed decoding TX attribute usage: 0x%2x", int(attr.Type))
|
||||
return
|
||||
|
@ -35,6 +46,8 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
|
|||
bw.WriteB(byte(attr.Type))
|
||||
switch attr.Type {
|
||||
case HighPriority:
|
||||
case OracleResponseT:
|
||||
attr.Value.EncodeBinary(bw)
|
||||
default:
|
||||
bw.Err = fmt.Errorf("failed encoding TX attribute usage: 0x%2x", attr.Type)
|
||||
}
|
||||
|
@ -42,9 +55,11 @@ func (attr *Attribute) EncodeBinary(bw *io.BinWriter) {
|
|||
|
||||
// MarshalJSON implements the json Marshaller interface.
|
||||
func (attr *Attribute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(attrJSON{
|
||||
Type: attr.Type.String(),
|
||||
})
|
||||
m := map[string]interface{}{"type": attr.Type.String()}
|
||||
if attr.Value != nil {
|
||||
attr.Value.toJSONMap(m)
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaller interface.
|
||||
|
@ -57,6 +72,12 @@ func (attr *Attribute) UnmarshalJSON(data []byte) error {
|
|||
switch aj.Type {
|
||||
case "HighPriority":
|
||||
attr.Type = HighPriority
|
||||
case "OracleResponse":
|
||||
attr.Type = OracleResponseT
|
||||
// Note: because `type` field will not be present in any attribute
|
||||
// value, we can unmarshal the same data. The overhead is minimal.
|
||||
attr.Value = new(OracleResponse)
|
||||
return json.Unmarshal(data, attr.Value)
|
||||
default:
|
||||
return errors.New("wrong Type")
|
||||
|
||||
|
|
66
pkg/core/transaction/attribute_test.go
Normal file
66
pkg/core/transaction/attribute_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package transaction
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAttribute_EncodeBinary(t *testing.T) {
|
||||
t.Run("HighPriority", func(t *testing.T) {
|
||||
attr := &Attribute{
|
||||
Type: HighPriority,
|
||||
}
|
||||
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
|
||||
})
|
||||
t.Run("OracleResponse", func(t *testing.T) {
|
||||
attr := &Attribute{
|
||||
Type: OracleResponseT,
|
||||
Value: &OracleResponse{
|
||||
ID: 0x1122334455,
|
||||
Code: Success,
|
||||
Result: []byte{1, 2, 3},
|
||||
},
|
||||
}
|
||||
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAttribute_MarshalJSON(t *testing.T) {
|
||||
t.Run("HighPriority", func(t *testing.T) {
|
||||
attr := &Attribute{Type: HighPriority}
|
||||
data, err := json.Marshal(attr)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{"type":"HighPriority"}`, string(data))
|
||||
|
||||
actual := new(Attribute)
|
||||
require.NoError(t, json.Unmarshal(data, actual))
|
||||
require.Equal(t, attr, actual)
|
||||
})
|
||||
t.Run("OracleResponse", func(t *testing.T) {
|
||||
res := []byte{1, 2, 3}
|
||||
attr := &Attribute{
|
||||
Type: OracleResponseT,
|
||||
Value: &OracleResponse{
|
||||
ID: 123,
|
||||
Code: Success,
|
||||
Result: res,
|
||||
},
|
||||
}
|
||||
data, err := json.Marshal(attr)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, `{
|
||||
"type":"OracleResponse",
|
||||
"id": 123,
|
||||
"code": 0,
|
||||
"result": "`+base64.StdEncoding.EncodeToString(res)+`"}`, string(data))
|
||||
|
||||
actual := new(Attribute)
|
||||
require.NoError(t, json.Unmarshal(data, actual))
|
||||
require.Equal(t, attr, actual)
|
||||
testserdes.EncodeDecodeBinary(t, attr, new(Attribute))
|
||||
})
|
||||
}
|
|
@ -1,11 +1,16 @@
|
|||
package transaction
|
||||
|
||||
//go:generate stringer -type=AttrType
|
||||
//go:generate stringer -type=AttrType -linecomment
|
||||
|
||||
// AttrType represents the purpose of the attribute.
|
||||
type AttrType uint8
|
||||
|
||||
// List of valid attribute types.
|
||||
const (
|
||||
HighPriority AttrType = 1
|
||||
HighPriority AttrType = 1
|
||||
OracleResponseT AttrType = 0x11 // OracleResponse
|
||||
)
|
||||
|
||||
func (a AttrType) allowMultiple() bool {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by "stringer -type AttrType"; DO NOT EDIT.
|
||||
// Code generated by "stringer -type=AttrType -linecomment"; DO NOT EDIT.
|
||||
|
||||
package transaction
|
||||
|
||||
|
@ -9,16 +9,21 @@ func _() {
|
|||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[HighPriority-1]
|
||||
_ = x[OracleResponseT-17]
|
||||
}
|
||||
|
||||
const _AttrType_name = "HighPriority"
|
||||
|
||||
var _AttrType_index = [...]uint8{0, 12}
|
||||
const (
|
||||
_AttrType_name_0 = "HighPriority"
|
||||
_AttrType_name_1 = "OracleResponse"
|
||||
)
|
||||
|
||||
func (i AttrType) String() string {
|
||||
i -= 1
|
||||
if i >= AttrType(len(_AttrType_index)-1) {
|
||||
return "AttrType(" + strconv.FormatInt(int64(i+1), 10) + ")"
|
||||
switch {
|
||||
case i == 1:
|
||||
return _AttrType_name_0
|
||||
case i == 17:
|
||||
return _AttrType_name_1
|
||||
default:
|
||||
return "AttrType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _AttrType_name[_AttrType_index[i]:_AttrType_index[i+1]]
|
||||
}
|
||||
|
|
66
pkg/core/transaction/oracle.go
Normal file
66
pkg/core/transaction/oracle.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package transaction
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||
)
|
||||
|
||||
// OracleResponseCode represents result code of oracle response.
|
||||
type OracleResponseCode byte
|
||||
|
||||
// OracleResponse represents oracle response.
|
||||
type OracleResponse struct {
|
||||
ID uint64 `json:"id"`
|
||||
Code OracleResponseCode `json:"code"`
|
||||
Result []byte `json:"result"`
|
||||
}
|
||||
|
||||
const maxResultSize = 1024
|
||||
|
||||
// Enumeration of possible oracle response types.
|
||||
const (
|
||||
Success OracleResponseCode = 0x00
|
||||
NotFound OracleResponseCode = 0x10
|
||||
Timeout OracleResponseCode = 0x12
|
||||
Forbidden OracleResponseCode = 0x14
|
||||
Error OracleResponseCode = 0xff
|
||||
)
|
||||
|
||||
// Various validation errors.
|
||||
var (
|
||||
ErrInvalidResponseCode = errors.New("invalid oracle response code")
|
||||
ErrInvalidResult = errors.New("oracle response != success, but result is not empty")
|
||||
)
|
||||
|
||||
// IsValid checks if c is valid response code.
|
||||
func (c OracleResponseCode) IsValid() bool {
|
||||
return c == Success || c == NotFound || c == Timeout || c == Forbidden || c == Error
|
||||
}
|
||||
|
||||
// DecodeBinary implements io.Serializable interface.
|
||||
func (r *OracleResponse) DecodeBinary(br *io.BinReader) {
|
||||
r.ID = br.ReadU64LE()
|
||||
r.Code = OracleResponseCode(br.ReadB())
|
||||
if !r.Code.IsValid() {
|
||||
br.Err = ErrInvalidResponseCode
|
||||
return
|
||||
}
|
||||
r.Result = br.ReadVarBytes(maxResultSize)
|
||||
if r.Code != Success && len(r.Result) > 0 {
|
||||
br.Err = ErrInvalidResult
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeBinary implements io.Serializable interface.
|
||||
func (r *OracleResponse) EncodeBinary(w *io.BinWriter) {
|
||||
w.WriteU64LE(r.ID)
|
||||
w.WriteB(byte(r.Code))
|
||||
w.WriteVarBytes(r.Result)
|
||||
}
|
||||
|
||||
func (r *OracleResponse) toJSONMap(m map[string]interface{}) {
|
||||
m["id"] = r.ID
|
||||
m["code"] = r.Code
|
||||
m["result"] = r.Result
|
||||
}
|
76
pkg/core/transaction/oracle_test.go
Normal file
76
pkg/core/transaction/oracle_test.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package transaction
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/internal/testserdes"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOracleResponse_EncodeBinary(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
r := &OracleResponse{
|
||||
ID: rand.Uint64(),
|
||||
Code: Success,
|
||||
Result: []byte{1, 2, 3, 4, 5},
|
||||
}
|
||||
testserdes.EncodeDecodeBinary(t, r, new(OracleResponse))
|
||||
})
|
||||
t.Run("ErrorCodes", func(t *testing.T) {
|
||||
codes := []OracleResponseCode{NotFound, Timeout, Forbidden, Error}
|
||||
for _, c := range codes {
|
||||
r := &OracleResponse{
|
||||
ID: rand.Uint64(),
|
||||
Code: c,
|
||||
Result: []byte{},
|
||||
}
|
||||
testserdes.EncodeDecodeBinary(t, r, new(OracleResponse))
|
||||
}
|
||||
})
|
||||
t.Run("Error", func(t *testing.T) {
|
||||
t.Run("InvalidCode", func(t *testing.T) {
|
||||
r := &OracleResponse{
|
||||
ID: rand.Uint64(),
|
||||
Code: 0x42,
|
||||
Result: []byte{},
|
||||
}
|
||||
bs, err := testserdes.EncodeBinary(r)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testserdes.DecodeBinary(bs, new(OracleResponse))
|
||||
require.True(t, errors.Is(err, ErrInvalidResponseCode), "got: %v", err)
|
||||
})
|
||||
t.Run("InvalidResult", func(t *testing.T) {
|
||||
r := &OracleResponse{
|
||||
ID: rand.Uint64(),
|
||||
Code: Error,
|
||||
Result: []byte{1},
|
||||
}
|
||||
bs, err := testserdes.EncodeBinary(r)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testserdes.DecodeBinary(bs, new(OracleResponse))
|
||||
require.True(t, errors.Is(err, ErrInvalidResult), "got: %v", err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestOracleResponse_toJSONMap(t *testing.T) {
|
||||
r := &OracleResponse{
|
||||
ID: rand.Uint64(),
|
||||
Code: Success,
|
||||
Result: []byte{1},
|
||||
}
|
||||
|
||||
b1, err := json.Marshal(r)
|
||||
require.NoError(t, err)
|
||||
|
||||
m := map[string]interface{}{}
|
||||
r.toJSONMap(m)
|
||||
b2, err := json.Marshal(m)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, string(b1), string(b2))
|
||||
}
|
|
@ -381,14 +381,14 @@ func (t *Transaction) isValid() error {
|
|||
}
|
||||
}
|
||||
}
|
||||
hasHighPrio := false
|
||||
attrs := map[AttrType]bool{}
|
||||
for i := range t.Attributes {
|
||||
switch t.Attributes[i].Type {
|
||||
case HighPriority:
|
||||
if hasHighPrio {
|
||||
return fmt.Errorf("%w: multiple high priority attributes", ErrInvalidAttribute)
|
||||
typ := t.Attributes[i].Type
|
||||
if !typ.allowMultiple() {
|
||||
if attrs[typ] {
|
||||
return fmt.Errorf("%w: multiple '%s' attributes", ErrInvalidAttribute, typ.String())
|
||||
}
|
||||
hasHighPrio = true
|
||||
attrs[typ] = true
|
||||
}
|
||||
}
|
||||
if len(t.Script) == 0 {
|
||||
|
|
|
@ -206,6 +206,14 @@ func TestTransaction_isValid(t *testing.T) {
|
|||
}
|
||||
require.True(t, errors.Is(tx.isValid(), ErrInvalidAttribute))
|
||||
})
|
||||
t.Run("MultipleOracle", func(t *testing.T) {
|
||||
tx := newTx()
|
||||
tx.Attributes = []Attribute{
|
||||
{Type: OracleResponseT},
|
||||
{Type: OracleResponseT},
|
||||
}
|
||||
require.True(t, errors.Is(tx.isValid(), ErrInvalidAttribute))
|
||||
})
|
||||
t.Run("NoScript", func(t *testing.T) {
|
||||
tx := newTx()
|
||||
tx.Script = []byte{}
|
||||
|
|
Loading…
Reference in a new issue