neo-go/pkg/smartcontract/parameter.go

466 lines
11 KiB
Go
Raw Normal View History

package smartcontract
import (
"encoding/base64"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
2021-02-18 15:13:03 +00:00
"io/ioutil"
"math"
"math/bits"
"strconv"
"strings"
"unicode/utf8"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/io"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// Parameter represents a smart contract parameter.
type Parameter struct {
2019-10-22 14:56:03 +00:00
// Type of the parameter.
Type ParamType `json:"type"`
// The actual value of the parameter.
Value interface{} `json:"value"`
}
// ParameterPair represents key-value pair, a slice of which is stored in
// MapType Parameter.
type ParameterPair struct {
Key Parameter `json:"key"`
Value Parameter `json:"value"`
}
// NewParameter returns a Parameter with proper initialized Value
// of the given ParamType.
func NewParameter(t ParamType) Parameter {
return Parameter{
Type: t,
Value: nil,
}
}
type rawParameter struct {
Type ParamType `json:"type"`
Value json.RawMessage `json:"value,omitempty"`
}
// MarshalJSON implements Marshaler interface.
func (p Parameter) MarshalJSON() ([]byte, error) {
var (
resultRawValue json.RawMessage
2020-03-03 14:22:15 +00:00
resultErr error
)
switch p.Type {
case BoolType, StringType, Hash160Type, Hash256Type:
resultRawValue, resultErr = json.Marshal(p.Value)
case IntegerType:
val, ok := p.Value.(int64)
if !ok {
resultErr = errors.New("invalid integer value")
break
}
valStr := strconv.FormatInt(val, 10)
resultRawValue = json.RawMessage(`"` + valStr + `"`)
case PublicKeyType, ByteArrayType, SignatureType:
if p.Value == nil {
resultRawValue = []byte("null")
} else if p.Type == PublicKeyType {
resultRawValue, resultErr = json.Marshal(hex.EncodeToString(p.Value.([]byte)))
} else {
resultRawValue, resultErr = json.Marshal(base64.StdEncoding.EncodeToString(p.Value.([]byte)))
}
case ArrayType:
var value = p.Value.([]Parameter)
if value == nil {
resultRawValue, resultErr = json.Marshal([]Parameter{})
} else {
resultRawValue, resultErr = json.Marshal(value)
}
case MapType:
ppair := p.Value.([]ParameterPair)
resultRawValue, resultErr = json.Marshal(ppair)
case InteropInterfaceType, AnyType:
resultRawValue = nil
default:
resultErr = fmt.Errorf("can't marshal %s", p.Type)
}
if resultErr != nil {
return nil, resultErr
}
return json.Marshal(rawParameter{
Type: p.Type,
Value: resultRawValue,
})
}
// UnmarshalJSON implements Unmarshaler interface.
func (p *Parameter) UnmarshalJSON(data []byte) (err error) {
var (
r rawParameter
i int64
s string
b []byte
boolean bool
)
if err = json.Unmarshal(data, &r); err != nil {
return
}
switch p.Type = r.Type; r.Type {
case BoolType:
if err = json.Unmarshal(r.Value, &boolean); err != nil {
return
}
p.Value = boolean
case ByteArrayType, PublicKeyType, SignatureType:
if err = json.Unmarshal(r.Value, &s); err != nil {
return
}
if r.Type == PublicKeyType {
b, err = hex.DecodeString(s)
} else {
b, err = base64.StdEncoding.DecodeString(s)
}
if err != nil {
return
}
p.Value = b
case StringType:
if err = json.Unmarshal(r.Value, &s); err != nil {
return
}
p.Value = s
case IntegerType:
if err = json.Unmarshal(r.Value, &i); err == nil {
p.Value = i
return
}
// sometimes integer comes as string
if err = json.Unmarshal(r.Value, &s); err != nil {
return
}
if i, err = strconv.ParseInt(s, 10, 64); err != nil {
return
}
p.Value = i
case ArrayType:
// https://github.com/neo-project/neo/blob/3d59ecca5a8deb057bdad94b3028a6d5e25ac088/neo/Network/RPC/RpcServer.cs#L67
var rs []Parameter
if err = json.Unmarshal(r.Value, &rs); err != nil {
return
}
p.Value = rs
case MapType:
var ppair []ParameterPair
if err = json.Unmarshal(r.Value, &ppair); err != nil {
return
}
p.Value = ppair
case Hash160Type:
var h util.Uint160
if err = json.Unmarshal(r.Value, &h); err != nil {
return
}
p.Value = h
case Hash256Type:
var h util.Uint256
if err = json.Unmarshal(r.Value, &h); err != nil {
return
}
p.Value = h
case InteropInterfaceType, AnyType:
// stub, ignore value, it can only be null
p.Value = nil
default:
return fmt.Errorf("can't unmarshal %s", p.Type)
}
return
}
// EncodeBinary implements io.Serializable interface.
func (p *Parameter) EncodeBinary(w *io.BinWriter) {
w.WriteB(byte(p.Type))
switch p.Type {
case BoolType:
w.WriteBool(p.Value.(bool))
case ByteArrayType, PublicKeyType, SignatureType:
if p.Value == nil {
w.WriteVarUint(math.MaxUint64)
} else {
w.WriteVarBytes(p.Value.([]byte))
}
case StringType:
w.WriteString(p.Value.(string))
case IntegerType:
w.WriteU64LE(uint64(p.Value.(int64)))
case ArrayType:
w.WriteArray(p.Value.([]Parameter))
case MapType:
w.WriteArray(p.Value.([]ParameterPair))
case Hash160Type:
w.WriteBytes(p.Value.(util.Uint160).BytesBE())
case Hash256Type:
w.WriteBytes(p.Value.(util.Uint256).BytesBE())
case InteropInterfaceType, AnyType:
default:
w.Err = fmt.Errorf("unknown type: %x", p.Type)
}
}
// DecodeBinary implements io.Serializable interface.
func (p *Parameter) DecodeBinary(r *io.BinReader) {
p.Type = ParamType(r.ReadB())
switch p.Type {
case BoolType:
p.Value = r.ReadBool()
case ByteArrayType, PublicKeyType, SignatureType:
ln := r.ReadVarUint()
if ln != math.MaxUint64 {
b := make([]byte, ln)
r.ReadBytes(b)
p.Value = b
}
case StringType:
p.Value = r.ReadString()
case IntegerType:
p.Value = int64(r.ReadU64LE())
case ArrayType:
ps := []Parameter{}
r.ReadArray(&ps)
p.Value = ps
case MapType:
ps := []ParameterPair{}
r.ReadArray(&ps)
p.Value = ps
case Hash160Type:
var u util.Uint160
r.ReadBytes(u[:])
p.Value = u
case Hash256Type:
var u util.Uint256
r.ReadBytes(u[:])
p.Value = u
case InteropInterfaceType, AnyType:
default:
r.Err = fmt.Errorf("unknown type: %x", p.Type)
}
}
// EncodeBinary implements io.Serializable interface.
func (p *ParameterPair) EncodeBinary(w *io.BinWriter) {
p.Key.EncodeBinary(w)
p.Value.EncodeBinary(w)
}
// DecodeBinary implements io.Serializable interface.
func (p *ParameterPair) DecodeBinary(r *io.BinReader) {
p.Key.DecodeBinary(r)
p.Value.DecodeBinary(r)
}
// Params is an array of Parameter (TODO: drop it?).
type Params []Parameter
// TryParseArray converts an array of Parameter into an array of more appropriate things.
func (p Params) TryParseArray(vals ...interface{}) error {
var (
err error
i int
par Parameter
)
if len(p) != len(vals) {
return errors.New("receiver array doesn't fit the Params length")
}
for i, par = range p {
if err = par.TryParse(vals[i]); err != nil {
return err
}
}
return nil
}
// TryParse converts one Parameter into something more appropriate.
func (p Parameter) TryParse(dest interface{}) error {
var (
err error
ok bool
data []byte
)
switch p.Type {
case ByteArrayType:
if data, ok = p.Value.([]byte); !ok {
return fmt.Errorf("failed to cast %s to []byte", p.Value)
}
switch dest := dest.(type) {
case *util.Uint160:
if *dest, err = util.Uint160DecodeBytesBE(data); err != nil {
return err
}
return nil
case *[]byte:
*dest = data
return nil
case *util.Uint256:
if *dest, err = util.Uint256DecodeBytesLE(data); err != nil {
return err
}
return nil
case *int64, *int32, *int16, *int8, *int, *uint64, *uint32, *uint16, *uint8, *uint:
var size int
switch dest.(type) {
case *int64, *uint64:
size = 64
case *int32, *uint32:
size = 32
case *int16, *uint16:
size = 16
case *int8, *uint8:
size = 8
case *int, *uint:
size = bits.UintSize
}
i, err := bytesToUint64(data, size)
if err != nil {
return err
}
switch dest := dest.(type) {
case *int64:
*dest = int64(i)
case *int32:
*dest = int32(i)
case *int16:
*dest = int16(i)
case *int8:
*dest = int8(i)
case *int:
*dest = int(i)
case *uint64:
*dest = i
case *uint32:
*dest = uint32(i)
case *uint16:
*dest = uint16(i)
case *uint8:
*dest = uint8(i)
case *uint:
*dest = uint(i)
}
case *string:
*dest = string(data)
return nil
default:
return fmt.Errorf("cannot cast param of type %s to type %s", p.Type, dest)
}
default:
return errors.New("cannot define param type")
}
return nil
}
func bytesToUint64(b []byte, size int) (uint64, error) {
var length = size / 8
if len(b) > length {
return 0, fmt.Errorf("input doesn't fit into %d bits", size)
}
if len(b) < length {
data := make([]byte, length)
copy(data, b)
return binary.LittleEndian.Uint64(data), nil
}
return binary.LittleEndian.Uint64(b), nil
}
// NewParameterFromString returns a new Parameter initialized from the given
// string in neo-go-specific format. It is intended to be used in user-facing
// interfaces and has some heuristics in it to simplify parameter passing. Exact
// syntax is documented in the cli documentation.
func NewParameterFromString(in string) (*Parameter, error) {
var (
char rune
val string
err error
r *strings.Reader
buf strings.Builder
escaped bool
hadType bool
res = &Parameter{}
2021-02-18 15:13:03 +00:00
typStr string
)
r = strings.NewReader(in)
for char, _, err = r.ReadRune(); err == nil && char != utf8.RuneError; char, _, err = r.ReadRune() {
if char == '\\' && !escaped {
escaped = true
continue
}
if char == ':' && !escaped && !hadType {
2021-02-18 15:13:03 +00:00
typStr = buf.String()
res.Type, err = ParseParamType(typStr)
if err != nil {
return nil, err
}
// We currently do not support following types:
if res.Type == ArrayType || res.Type == MapType || res.Type == InteropInterfaceType || res.Type == VoidType {
return nil, fmt.Errorf("unsupported parameter type %s", res.Type)
}
buf.Reset()
hadType = true
continue
}
escaped = false
// We don't care about length and it never fails.
_, _ = buf.WriteRune(char)
}
if char == utf8.RuneError {
return nil, errors.New("bad UTF-8 string")
}
// The only other error `ReadRune` returns is io.EOF, which is fine and
// expected, so we don't check err here.
val = buf.String()
if !hadType {
res.Type = inferParamType(val)
}
2021-02-18 15:13:03 +00:00
if res.Type == ByteArrayType && typStr == fileBytesParamType {
res.Value, err = ioutil.ReadFile(val)
if err != nil {
return nil, fmt.Errorf("failed to read '%s' parameter from file '%s': %w", fileBytesParamType, val, err)
}
return res, nil
}
res.Value, err = adjustValToType(res.Type, val)
if err != nil {
return nil, err
}
return res, nil
2020-03-03 14:22:15 +00:00
}
// ExpandParameterToEmitable converts parameter to a type which can be handled as
// an array item by emit.Array. It correlates with the way RPC server handles
// FuncParams for invoke* calls inside the request.ExpandArrayIntoScript function.
func ExpandParameterToEmitable(param Parameter) (interface{}, error) {
var err error
switch t := param.Type; t {
case PublicKeyType:
return param.Value.(*keys.PublicKey).Bytes(), nil
case ArrayType:
arr := param.Value.([]Parameter)
res := make([]interface{}, len(arr))
for i := range arr {
res[i], err = ExpandParameterToEmitable(arr[i])
if err != nil {
return nil, err
}
}
return res, nil
case MapType, InteropInterfaceType, UnknownType, AnyType, VoidType:
return nil, fmt.Errorf("unsupported parameter type: %s", t.String())
default:
return param.Value, nil
}
}