2018-03-04 13:56:49 +00:00
|
|
|
package smartcontract
|
|
|
|
|
2019-11-14 08:14:06 +00:00
|
|
|
import (
|
2020-06-05 12:45:15 +00:00
|
|
|
"encoding/base64"
|
2020-02-21 14:34:18 +00:00
|
|
|
"encoding/binary"
|
2019-11-27 09:52:15 +00:00
|
|
|
"encoding/hex"
|
2020-02-20 10:25:01 +00:00
|
|
|
"encoding/json"
|
2020-08-06 14:44:08 +00:00
|
|
|
"errors"
|
2020-03-23 11:53:17 +00:00
|
|
|
"fmt"
|
|
|
|
"math"
|
2020-02-21 14:34:18 +00:00
|
|
|
"math/bits"
|
2019-11-27 09:52:15 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"unicode/utf8"
|
|
|
|
|
2020-03-23 11:53:17 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
2020-03-03 14:21:42 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
2018-03-04 13:56:49 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Parameter represents a smart contract parameter.
|
|
|
|
type Parameter struct {
|
2019-10-22 14:56:03 +00:00
|
|
|
// Type of the parameter.
|
2018-05-04 17:15:35 +00:00
|
|
|
Type ParamType `json:"type"`
|
2018-03-04 13:56:49 +00:00
|
|
|
// The actual value of the parameter.
|
2018-05-04 17:15:35 +00:00
|
|
|
Value interface{} `json:"value"`
|
2018-03-04 13:56:49 +00:00
|
|
|
}
|
|
|
|
|
2020-03-30 15:24:06 +00:00
|
|
|
// 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"`
|
|
|
|
}
|
|
|
|
|
2018-03-04 13:56:49 +00:00
|
|
|
// NewParameter returns a Parameter with proper initialized Value
|
|
|
|
// of the given ParamType.
|
|
|
|
func NewParameter(t ParamType) Parameter {
|
|
|
|
return Parameter{
|
|
|
|
Type: t,
|
|
|
|
Value: nil,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-21 14:34:18 +00:00
|
|
|
type rawParameter struct {
|
|
|
|
Type ParamType `json:"type"`
|
2020-06-19 07:44:05 +00:00
|
|
|
Value json.RawMessage `json:"value,omitempty"`
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
|
|
|
|
2020-02-27 14:38:17 +00:00
|
|
|
// MarshalJSON implements Marshaler interface.
|
2020-06-23 13:11:49 +00:00
|
|
|
func (p Parameter) MarshalJSON() ([]byte, error) {
|
2020-02-27 14:38:17 +00:00
|
|
|
var (
|
|
|
|
resultRawValue json.RawMessage
|
2020-03-03 14:22:15 +00:00
|
|
|
resultErr error
|
2020-02-27 14:38:17 +00:00
|
|
|
)
|
|
|
|
switch p.Type {
|
2020-04-01 17:31:29 +00:00
|
|
|
case BoolType, StringType, Hash160Type, Hash256Type:
|
2020-02-27 14:38:17 +00:00
|
|
|
resultRawValue, resultErr = json.Marshal(p.Value)
|
2020-03-17 13:04:49 +00:00
|
|
|
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 + `"`)
|
2020-02-27 14:38:17 +00:00
|
|
|
case PublicKeyType, ByteArrayType, SignatureType:
|
2020-03-04 16:47:10 +00:00
|
|
|
if p.Value == nil {
|
|
|
|
resultRawValue = []byte("null")
|
2020-06-05 12:45:15 +00:00
|
|
|
} else if p.Type == PublicKeyType {
|
2020-03-04 16:47:10 +00:00
|
|
|
resultRawValue, resultErr = json.Marshal(hex.EncodeToString(p.Value.([]byte)))
|
2020-06-05 12:45:15 +00:00
|
|
|
} else {
|
|
|
|
resultRawValue, resultErr = json.Marshal(base64.StdEncoding.EncodeToString(p.Value.([]byte)))
|
2020-03-04 16:47:10 +00:00
|
|
|
}
|
2020-02-27 14:38:17 +00:00
|
|
|
case ArrayType:
|
2020-03-30 21:27:58 +00:00
|
|
|
var value = p.Value.([]Parameter)
|
2020-06-23 13:11:49 +00:00
|
|
|
if value == nil {
|
|
|
|
resultRawValue, resultErr = json.Marshal([]Parameter{})
|
|
|
|
} else {
|
|
|
|
resultRawValue, resultErr = json.Marshal(value)
|
|
|
|
}
|
2020-02-27 14:38:17 +00:00
|
|
|
case MapType:
|
2020-03-30 15:24:06 +00:00
|
|
|
ppair := p.Value.([]ParameterPair)
|
|
|
|
resultRawValue, resultErr = json.Marshal(ppair)
|
2020-06-03 15:56:20 +00:00
|
|
|
case InteropInterfaceType, AnyType:
|
2020-06-19 07:44:05 +00:00
|
|
|
resultRawValue = nil
|
2020-02-27 14:38:17 +00:00
|
|
|
default:
|
2020-08-06 14:44:08 +00:00
|
|
|
resultErr = fmt.Errorf("can't marshal %s", p.Type)
|
2020-02-27 14:38:17 +00:00
|
|
|
}
|
|
|
|
if resultErr != nil {
|
|
|
|
return nil, resultErr
|
|
|
|
}
|
|
|
|
return json.Marshal(rawParameter{
|
|
|
|
Type: p.Type,
|
|
|
|
Value: resultRawValue,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-02-21 14:34:18 +00:00
|
|
|
// UnmarshalJSON implements Unmarshaler interface.
|
|
|
|
func (p *Parameter) UnmarshalJSON(data []byte) (err error) {
|
|
|
|
var (
|
2020-02-27 14:38:17 +00:00
|
|
|
r rawParameter
|
|
|
|
i int64
|
|
|
|
s string
|
|
|
|
b []byte
|
|
|
|
boolean bool
|
2020-02-21 14:34:18 +00:00
|
|
|
)
|
|
|
|
if err = json.Unmarshal(data, &r); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch p.Type = r.Type; r.Type {
|
2020-02-27 14:38:17 +00:00
|
|
|
case BoolType:
|
|
|
|
if err = json.Unmarshal(r.Value, &boolean); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
p.Value = boolean
|
2020-03-04 09:30:48 +00:00
|
|
|
case ByteArrayType, PublicKeyType, SignatureType:
|
2020-02-21 14:34:18 +00:00
|
|
|
if err = json.Unmarshal(r.Value, &s); err != nil {
|
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-06-05 12:45:15 +00:00
|
|
|
if r.Type == PublicKeyType {
|
|
|
|
b, err = hex.DecodeString(s)
|
|
|
|
} else {
|
|
|
|
b, err = base64.StdEncoding.DecodeString(s)
|
|
|
|
}
|
|
|
|
if err != nil {
|
2020-02-21 14:34:18 +00:00
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
p.Value = b
|
|
|
|
case StringType:
|
|
|
|
if err = json.Unmarshal(r.Value, &s); err != nil {
|
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
p.Value = s
|
2019-11-27 09:52:15 +00:00
|
|
|
case IntegerType:
|
2020-02-21 14:34:18 +00:00
|
|
|
if err = json.Unmarshal(r.Value, &i); err == nil {
|
|
|
|
p.Value = i
|
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
// sometimes integer comes as string
|
|
|
|
if err = json.Unmarshal(r.Value, &s); err != nil {
|
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
if i, err = strconv.ParseInt(s, 10, 64); err != nil {
|
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
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
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
p.Value = rs
|
2020-02-27 14:38:17 +00:00
|
|
|
case MapType:
|
2020-03-30 15:24:06 +00:00
|
|
|
var ppair []ParameterPair
|
|
|
|
if err = json.Unmarshal(r.Value, &ppair); err != nil {
|
2020-02-27 14:38:17 +00:00
|
|
|
return
|
|
|
|
}
|
2020-03-30 15:24:06 +00:00
|
|
|
p.Value = ppair
|
2020-02-21 14:34:18 +00:00
|
|
|
case Hash160Type:
|
|
|
|
var h util.Uint160
|
|
|
|
if err = json.Unmarshal(r.Value, &h); err != nil {
|
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-04-01 17:31:29 +00:00
|
|
|
p.Value = h
|
2020-02-21 14:34:18 +00:00
|
|
|
case Hash256Type:
|
|
|
|
var h util.Uint256
|
|
|
|
if err = json.Unmarshal(r.Value, &h); err != nil {
|
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
p.Value = h
|
2020-06-03 15:56:20 +00:00
|
|
|
case InteropInterfaceType, AnyType:
|
2020-05-10 21:55:15 +00:00
|
|
|
// stub, ignore value, it can only be null
|
|
|
|
p.Value = nil
|
2019-11-27 09:52:15 +00:00
|
|
|
default:
|
2020-08-06 14:44:08 +00:00
|
|
|
return fmt.Errorf("can't unmarshal %s", p.Type)
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
return
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
|
|
|
|
2020-03-23 11:53:17 +00:00
|
|
|
// 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:
|
2020-03-30 15:24:06 +00:00
|
|
|
w.WriteArray(p.Value.([]ParameterPair))
|
2020-03-23 11:53:17 +00:00
|
|
|
case Hash160Type:
|
|
|
|
w.WriteBytes(p.Value.(util.Uint160).BytesBE())
|
|
|
|
case Hash256Type:
|
|
|
|
w.WriteBytes(p.Value.(util.Uint256).BytesBE())
|
2020-06-23 21:05:27 +00:00
|
|
|
case InteropInterfaceType, AnyType:
|
2020-03-23 11:53:17 +00:00
|
|
|
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:
|
2020-03-30 15:24:06 +00:00
|
|
|
ps := []ParameterPair{}
|
|
|
|
r.ReadArray(&ps)
|
|
|
|
p.Value = ps
|
2020-03-23 11:53:17 +00:00
|
|
|
case Hash160Type:
|
|
|
|
var u util.Uint160
|
|
|
|
r.ReadBytes(u[:])
|
|
|
|
p.Value = u
|
|
|
|
case Hash256Type:
|
|
|
|
var u util.Uint256
|
|
|
|
r.ReadBytes(u[:])
|
|
|
|
p.Value = u
|
2020-06-23 21:05:27 +00:00
|
|
|
case InteropInterfaceType, AnyType:
|
2020-03-23 11:53:17 +00:00
|
|
|
default:
|
|
|
|
r.Err = fmt.Errorf("unknown type: %x", p.Type)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-30 15:24:06 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
|
2020-02-21 14:34:18 +00:00
|
|
|
// Params is an array of Parameter (TODO: drop it?).
|
|
|
|
type Params []Parameter
|
2019-11-27 09:52:15 +00:00
|
|
|
|
2020-02-21 14:34:18 +00:00
|
|
|
// 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")
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
for i, par = range p {
|
|
|
|
if err = par.TryParse(vals[i]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
2019-11-27 09:52:15 +00:00
|
|
|
|
2020-02-21 14:34:18 +00:00
|
|
|
// 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 {
|
2020-08-06 14:44:08 +00:00
|
|
|
return fmt.Errorf("failed to cast %s to []byte", p.Value)
|
2020-02-21 14:34:18 +00:00
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
2019-11-27 09:52:15 +00:00
|
|
|
|
2020-02-21 14:34:18 +00:00
|
|
|
i, err := bytesToUint64(data, size)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-11-27 09:52:15 +00:00
|
|
|
|
2020-02-21 14:34:18 +00:00
|
|
|
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
|
2019-11-27 09:52:15 +00:00
|
|
|
default:
|
2020-08-06 14:44:08 +00:00
|
|
|
return fmt.Errorf("cannot cast param of type %s to type %s", p.Type, dest)
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
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 {
|
2020-08-06 14:44:08 +00:00
|
|
|
return 0, fmt.Errorf("input doesn't fit into %d bits", size)
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
if len(b) < length {
|
|
|
|
data := make([]byte, length)
|
|
|
|
copy(data, b)
|
|
|
|
return binary.LittleEndian.Uint64(data), nil
|
|
|
|
}
|
|
|
|
return binary.LittleEndian.Uint64(b), nil
|
2019-11-27 09:52:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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{}
|
|
|
|
)
|
|
|
|
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()
|
2020-02-21 14:34:18 +00:00
|
|
|
res.Type, err = ParseParamType(typStr)
|
2019-11-27 09:52:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-02-21 14:34:18 +00:00
|
|
|
// We currently do not support following types:
|
|
|
|
if res.Type == ArrayType || res.Type == MapType || res.Type == InteropInterfaceType || res.Type == VoidType {
|
2020-08-06 14:44:08 +00:00
|
|
|
return nil, fmt.Errorf("unsupported parameter type %s", res.Type)
|
2020-02-21 14:34:18 +00:00
|
|
|
}
|
2019-11-27 09:52:15 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
res.Value, err = adjustValToType(res.Type, val)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return res, nil
|
2020-03-03 14:22:15 +00:00
|
|
|
}
|