mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-26 19:42:23 +00:00
7eb87afab8
Share parameters parsing code between 'contract invokefunction' and 'vm run' commands. It allows VM CLI to parse more complicated parameter types including arrays and file-backed bytestrings.
414 lines
11 KiB
Go
414 lines
11 KiB
Go
package smartcontract
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
)
|
|
|
|
// Parameter represents a smart contract parameter.
|
|
type Parameter struct {
|
|
// Type of the parameter.
|
|
Type ParamType `json:"type"`
|
|
// The actual value of the parameter.
|
|
Value interface{} `json:"value"`
|
|
}
|
|
|
|
// ParameterPair represents a 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 a 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 the Marshaler interface.
|
|
func (p Parameter) MarshalJSON() ([]byte, error) {
|
|
var (
|
|
resultRawValue json.RawMessage
|
|
resultErr error
|
|
)
|
|
if p.Value == nil {
|
|
if _, ok := validParamTypes[p.Type]; ok && p.Type != UnknownType {
|
|
return json.Marshal(rawParameter{Type: p.Type})
|
|
}
|
|
return nil, fmt.Errorf("can't marshal %s", p.Type)
|
|
}
|
|
switch p.Type {
|
|
case BoolType, StringType, Hash160Type, Hash256Type:
|
|
resultRawValue, resultErr = json.Marshal(p.Value)
|
|
case IntegerType:
|
|
val, ok := p.Value.(*big.Int)
|
|
if !ok {
|
|
resultErr = errors.New("invalid integer value")
|
|
break
|
|
}
|
|
resultRawValue = json.RawMessage(`"` + val.String() + `"`)
|
|
case PublicKeyType, ByteArrayType, SignatureType:
|
|
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 the 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
|
|
}
|
|
p.Type = r.Type
|
|
p.Value = nil
|
|
if len(r.Value) == 0 || bytes.Equal(r.Value, []byte("null")) {
|
|
return
|
|
}
|
|
switch 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 = big.NewInt(i)
|
|
return
|
|
}
|
|
// sometimes integer comes as string
|
|
if jErr := json.Unmarshal(r.Value, &s); jErr != nil {
|
|
return jErr
|
|
}
|
|
bi, ok := new(big.Int).SetString(s, 10)
|
|
if !ok {
|
|
// In this case previous err should mean string contains non-digit characters.
|
|
return err
|
|
}
|
|
err = stackitem.CheckIntegerSize(bi)
|
|
if err == nil {
|
|
p.Value = bi
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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. The 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{}
|
|
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 {
|
|
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)
|
|
}
|
|
if res.Type == ByteArrayType && typStr == fileBytesParamType {
|
|
res.Value, err = os.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
|
|
}
|
|
|
|
// NewParameterFromValue infers Parameter type from the value given and adjusts
|
|
// the value if needed. It does not copy the value if it can avoid doing so. All
|
|
// regular integers, util.*, keys.PublicKey*, string and bool types are supported,
|
|
// slice of byte slices is accepted and converted as well.
|
|
func NewParameterFromValue(value interface{}) (Parameter, error) {
|
|
var result = Parameter{
|
|
Value: value,
|
|
}
|
|
|
|
switch v := value.(type) {
|
|
case []byte:
|
|
result.Type = ByteArrayType
|
|
case string:
|
|
result.Type = StringType
|
|
case bool:
|
|
result.Type = BoolType
|
|
case *big.Int:
|
|
result.Type = IntegerType
|
|
case int8:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(int64(v))
|
|
case byte:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(int64(v))
|
|
case int16:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(int64(v))
|
|
case uint16:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(int64(v))
|
|
case int32:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(int64(v))
|
|
case uint32:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(int64(v))
|
|
case int:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(int64(v))
|
|
case uint:
|
|
result.Type = IntegerType
|
|
result.Value = new(big.Int).SetUint64(uint64(v))
|
|
case int64:
|
|
result.Type = IntegerType
|
|
result.Value = big.NewInt(v)
|
|
case uint64:
|
|
result.Type = IntegerType
|
|
result.Value = new(big.Int).SetUint64(v)
|
|
case *Parameter:
|
|
result = *v
|
|
case Parameter:
|
|
result = v
|
|
case util.Uint160:
|
|
result.Type = Hash160Type
|
|
case util.Uint256:
|
|
result.Type = Hash256Type
|
|
case *util.Uint160:
|
|
if v != nil {
|
|
return NewParameterFromValue(*v)
|
|
}
|
|
result.Type = AnyType
|
|
result.Value = nil
|
|
case *util.Uint256:
|
|
if v != nil {
|
|
return NewParameterFromValue(*v)
|
|
}
|
|
result.Type = AnyType
|
|
result.Value = nil
|
|
case keys.PublicKey:
|
|
return NewParameterFromValue(&v)
|
|
case *keys.PublicKey:
|
|
result.Type = PublicKeyType
|
|
result.Value = v.Bytes()
|
|
case [][]byte:
|
|
arr := make([]Parameter, 0, len(v))
|
|
for i := range v {
|
|
// We know the type exactly, so error is not possible.
|
|
elem, _ := NewParameterFromValue(v[i])
|
|
arr = append(arr, elem)
|
|
}
|
|
result.Type = ArrayType
|
|
result.Value = arr
|
|
case []Parameter:
|
|
arr := make([]Parameter, len(v))
|
|
copy(arr, v)
|
|
result.Type = ArrayType
|
|
result.Value = arr
|
|
case []*keys.PublicKey:
|
|
return NewParameterFromValue(keys.PublicKeys(v))
|
|
case keys.PublicKeys:
|
|
arr := make([]Parameter, 0, len(v))
|
|
for i := range v {
|
|
// We know the type exactly, so error is not possible.
|
|
elem, _ := NewParameterFromValue(v[i])
|
|
arr = append(arr, elem)
|
|
}
|
|
result.Type = ArrayType
|
|
result.Value = arr
|
|
case []interface{}:
|
|
arr, err := NewParametersFromValues(v...)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.Type = ArrayType
|
|
result.Value = arr
|
|
default:
|
|
return result, fmt.Errorf("unsupported parameter %T", value)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// NewParametersFromValues is similar to NewParameterFromValue except that it
|
|
// works with multiple values and returns a simple slice of Parameter.
|
|
func NewParametersFromValues(values ...interface{}) ([]Parameter, error) {
|
|
res := make([]Parameter, 0, len(values))
|
|
for i := range values {
|
|
elem, err := NewParameterFromValue(values[i])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = append(res, elem)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// ExpandParameterToEmitable converts a parameter to a type which can be handled as
|
|
// an array item by emit.Array. It correlates with the way an 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 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, VoidType:
|
|
return nil, fmt.Errorf("unsupported parameter type: %s", t.String())
|
|
default:
|
|
return param.Value, nil
|
|
}
|
|
}
|
|
|
|
// ToStackItem converts smartcontract parameter to stackitem.Item.
|
|
func (p *Parameter) ToStackItem() (stackitem.Item, error) {
|
|
e, err := ExpandParameterToEmitable(*p)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return stackitem.Make(e), nil
|
|
}
|