2020-09-18 13:26:36 +00:00
|
|
|
package native
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/binary"
|
|
|
|
"errors"
|
2020-10-21 10:02:46 +00:00
|
|
|
"fmt"
|
2020-11-05 16:34:48 +00:00
|
|
|
"math"
|
2020-09-18 13:26:36 +00:00
|
|
|
"math/big"
|
|
|
|
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Oracle represents Oracle native contract.
|
|
|
|
type Oracle struct {
|
|
|
|
interop.ContractMD
|
|
|
|
GAS *GAS
|
|
|
|
NEO *NEO
|
2020-09-24 12:46:31 +00:00
|
|
|
|
2020-10-01 15:17:09 +00:00
|
|
|
Desig *Designate
|
2020-09-18 13:26:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
oracleContractID = -4
|
|
|
|
oracleName = "Oracle"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
maxURLLength = 256
|
|
|
|
maxFilterLength = 128
|
|
|
|
maxCallbackLength = 32
|
|
|
|
maxUserDataLength = 512
|
2020-10-21 10:02:46 +00:00
|
|
|
// maxRequestsCount is the maximum number of requests per URL
|
|
|
|
maxRequestsCount = 256
|
2020-09-18 13:26:36 +00:00
|
|
|
|
|
|
|
oracleRequestPrice = 5000_0000
|
|
|
|
)
|
|
|
|
|
2020-10-02 06:51:55 +00:00
|
|
|
var (
|
|
|
|
oracleInvokeScript []byte
|
|
|
|
oracleScript []byte
|
|
|
|
)
|
2020-09-18 13:26:36 +00:00
|
|
|
|
|
|
|
func init() {
|
|
|
|
w := io.NewBufBinWriter()
|
|
|
|
emit.String(w.BinWriter, oracleName)
|
|
|
|
emit.Syscall(w.BinWriter, interopnames.NeoNativeCall)
|
2020-10-02 06:51:55 +00:00
|
|
|
oracleInvokeScript = w.Bytes()
|
|
|
|
h := hash.Hash160(oracleInvokeScript)
|
2020-09-18 13:26:36 +00:00
|
|
|
|
2020-10-02 06:51:55 +00:00
|
|
|
w = io.NewBufBinWriter()
|
2020-09-18 13:26:36 +00:00
|
|
|
emit.Int(w.BinWriter, 0)
|
2020-10-02 08:30:15 +00:00
|
|
|
emit.Opcodes(w.BinWriter, opcode.NEWARRAY)
|
2020-09-18 13:26:36 +00:00
|
|
|
emit.String(w.BinWriter, "finish")
|
|
|
|
emit.Bytes(w.BinWriter, h.BytesBE())
|
|
|
|
emit.Syscall(w.BinWriter, interopnames.SystemContractCall)
|
|
|
|
oracleScript = w.Bytes()
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
prefixIDList = []byte{6}
|
|
|
|
prefixRequest = []byte{7}
|
|
|
|
prefixRequestID = []byte{9}
|
|
|
|
)
|
|
|
|
|
|
|
|
// Various validation errors.
|
|
|
|
var (
|
|
|
|
ErrBigArgument = errors.New("some of the arguments are invalid")
|
|
|
|
ErrInvalidWitness = errors.New("witness check failed")
|
|
|
|
ErrNotEnoughGas = errors.New("gas limit exceeded")
|
|
|
|
ErrRequestNotFound = errors.New("oracle request not found")
|
|
|
|
ErrResponseNotFound = errors.New("oracle response not found")
|
|
|
|
)
|
|
|
|
|
2020-10-02 06:51:55 +00:00
|
|
|
// GetOracleInvokeScript returns oracle contract script.
|
|
|
|
func GetOracleInvokeScript() []byte {
|
|
|
|
b := make([]byte, len(oracleInvokeScript))
|
|
|
|
copy(b, oracleInvokeScript)
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
2020-09-18 13:26:36 +00:00
|
|
|
// GetOracleResponseScript returns script for transaction with oracle response.
|
|
|
|
func GetOracleResponseScript() []byte {
|
|
|
|
b := make([]byte, len(oracleScript))
|
|
|
|
copy(b, oracleScript)
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
func newOracle() *Oracle {
|
|
|
|
o := &Oracle{ContractMD: *interop.NewContractMD(oracleName)}
|
|
|
|
o.ContractID = oracleContractID
|
|
|
|
|
|
|
|
desc := newDescriptor("request", smartcontract.VoidType,
|
|
|
|
manifest.NewParameter("url", smartcontract.StringType),
|
|
|
|
manifest.NewParameter("filter", smartcontract.StringType),
|
|
|
|
manifest.NewParameter("callback", smartcontract.StringType),
|
|
|
|
manifest.NewParameter("userData", smartcontract.AnyType),
|
|
|
|
manifest.NewParameter("gasForResponse", smartcontract.IntegerType))
|
|
|
|
md := newMethodAndPrice(o.request, oracleRequestPrice, smartcontract.AllowModifyStates)
|
|
|
|
o.AddMethod(md, desc, false)
|
|
|
|
|
2020-10-09 12:06:28 +00:00
|
|
|
desc = newDescriptor("name", smartcontract.StringType)
|
|
|
|
md = newMethodAndPrice(nameMethod(oracleName), 0, smartcontract.NoneFlag)
|
|
|
|
o.AddMethod(md, desc, true)
|
|
|
|
|
2020-09-18 13:26:36 +00:00
|
|
|
desc = newDescriptor("finish", smartcontract.VoidType)
|
|
|
|
md = newMethodAndPrice(o.finish, 0, smartcontract.AllowModifyStates)
|
|
|
|
o.AddMethod(md, desc, false)
|
|
|
|
|
|
|
|
desc = newDescriptor("verify", smartcontract.BoolType)
|
|
|
|
md = newMethodAndPrice(o.verify, 100_0000, smartcontract.NoneFlag)
|
|
|
|
o.AddMethod(md, desc, false)
|
|
|
|
|
|
|
|
pp := chainOnPersist(postPersistBase, o.PostPersist)
|
|
|
|
desc = newDescriptor("postPersist", smartcontract.VoidType)
|
|
|
|
md = newMethodAndPrice(getOnPersistWrapper(pp), 0, smartcontract.AllowModifyStates)
|
|
|
|
o.AddMethod(md, desc, false)
|
|
|
|
|
2020-11-05 19:51:08 +00:00
|
|
|
o.AddEvent("OracleRequest", manifest.NewParameter("Id", smartcontract.IntegerType),
|
|
|
|
manifest.NewParameter("RequestContract", smartcontract.Hash160Type),
|
|
|
|
manifest.NewParameter("Url", smartcontract.StringType),
|
|
|
|
manifest.NewParameter("Filter", smartcontract.StringType))
|
|
|
|
o.AddEvent("OracleResponse", manifest.NewParameter("Id", smartcontract.IntegerType),
|
|
|
|
manifest.NewParameter("OriginalTx", smartcontract.Hash256Type))
|
|
|
|
|
2020-09-18 13:26:36 +00:00
|
|
|
return o
|
|
|
|
}
|
|
|
|
|
|
|
|
// PostPersist represents `postPersist` method.
|
|
|
|
func (o *Oracle) PostPersist(ic *interop.Context) error {
|
|
|
|
var nodes keys.PublicKeys
|
|
|
|
var reward []big.Int
|
|
|
|
single := new(big.Int).SetUint64(oracleRequestPrice)
|
|
|
|
for _, tx := range ic.Block.Transactions {
|
|
|
|
resp := getResponse(tx)
|
|
|
|
if resp == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
reqKey := makeRequestKey(resp.ID)
|
2020-10-07 12:06:10 +00:00
|
|
|
req := new(state.OracleRequest)
|
2020-09-18 13:26:36 +00:00
|
|
|
if err := o.getSerializableFromDAO(ic.DAO, reqKey, req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := ic.DAO.DeleteStorageItem(o.ContractID, reqKey); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
idKey := makeIDListKey(req.URL)
|
|
|
|
idList := new(IDList)
|
|
|
|
if err := o.getSerializableFromDAO(ic.DAO, idKey, idList); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if !idList.Remove(resp.ID) {
|
|
|
|
return errors.New("response ID wasn't found")
|
|
|
|
}
|
|
|
|
|
|
|
|
var err error
|
|
|
|
if len(*idList) == 0 {
|
|
|
|
err = ic.DAO.DeleteStorageItem(o.ContractID, idKey)
|
|
|
|
} else {
|
|
|
|
si := &state.StorageItem{Value: idList.Bytes()}
|
|
|
|
err = ic.DAO.PutStorageItem(o.ContractID, idKey, si)
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if nodes == nil {
|
2020-10-01 15:17:09 +00:00
|
|
|
nodes, err = o.GetOracleNodes(ic.DAO)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-09-18 13:26:36 +00:00
|
|
|
reward = make([]big.Int, len(nodes))
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(reward) > 0 {
|
|
|
|
index := resp.ID % uint64(len(nodes))
|
|
|
|
reward[index].Add(&reward[index], single)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for i := range reward {
|
|
|
|
o.GAS.mint(ic, nodes[i].GetScriptHash(), &reward[i])
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Metadata returns contract metadata.
|
|
|
|
func (o *Oracle) Metadata() *interop.ContractMD {
|
|
|
|
return &o.ContractMD
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize initializes Oracle contract.
|
|
|
|
func (o *Oracle) Initialize(ic *interop.Context) error {
|
2020-11-16 16:09:34 +00:00
|
|
|
si := &state.StorageItem{Value: make([]byte, 8)} // uint64(0) LE
|
2020-09-18 13:26:36 +00:00
|
|
|
return ic.DAO.PutStorageItem(o.ContractID, prefixRequestID, si)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getResponse(tx *transaction.Transaction) *transaction.OracleResponse {
|
|
|
|
for i := range tx.Attributes {
|
|
|
|
if tx.Attributes[i].Type == transaction.OracleResponseT {
|
|
|
|
return tx.Attributes[i].Value.(*transaction.OracleResponse)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Oracle) finish(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
|
|
err := o.FinishInternal(ic)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return stackitem.Null{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FinishInternal processes oracle response.
|
|
|
|
func (o *Oracle) FinishInternal(ic *interop.Context) error {
|
|
|
|
resp := getResponse(ic.Tx)
|
|
|
|
if resp == nil {
|
|
|
|
return ErrResponseNotFound
|
|
|
|
}
|
|
|
|
req, err := o.GetRequestInternal(ic.DAO, resp.ID)
|
|
|
|
if err != nil {
|
|
|
|
return ErrRequestNotFound
|
|
|
|
}
|
|
|
|
|
2020-11-05 19:51:08 +00:00
|
|
|
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
|
|
|
|
ScriptHash: o.Hash,
|
|
|
|
Name: "OracleResponse",
|
|
|
|
Item: stackitem.NewArray([]stackitem.Item{
|
|
|
|
stackitem.Make(resp.ID),
|
|
|
|
stackitem.Make(req.OriginalTxID.BytesBE()),
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
|
2020-09-18 13:26:36 +00:00
|
|
|
r := io.NewBinReaderFromBuf(req.UserData)
|
|
|
|
userData := stackitem.DecodeBinaryStackItem(r)
|
|
|
|
args := stackitem.NewArray([]stackitem.Item{
|
|
|
|
stackitem.Make(req.URL),
|
|
|
|
stackitem.Make(userData),
|
|
|
|
stackitem.Make(resp.Code),
|
|
|
|
stackitem.Make(resp.Result),
|
|
|
|
})
|
|
|
|
ic.VM.Estack().PushVal(args)
|
|
|
|
ic.VM.Estack().PushVal(req.CallbackMethod)
|
|
|
|
ic.VM.Estack().PushVal(req.CallbackContract.BytesBE())
|
|
|
|
return contract.Call(ic)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Oracle) request(ic *interop.Context, args []stackitem.Item) stackitem.Item {
|
|
|
|
url, err := stackitem.ToString(args[0])
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
filter, err := stackitem.ToString(args[1])
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
cb, err := stackitem.ToString(args[2])
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
userData := args[3]
|
|
|
|
gas, err := args[4].TryInteger()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if err := o.RequestInternal(ic, url, filter, cb, userData, gas); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return stackitem.Null{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// RequestInternal processes oracle request.
|
|
|
|
func (o *Oracle) RequestInternal(ic *interop.Context, url, filter, cb string, userData stackitem.Item, gas *big.Int) error {
|
|
|
|
if len(url) > maxURLLength || len(filter) > maxFilterLength || len(cb) > maxCallbackLength || gas.Uint64() < 1000_0000 {
|
|
|
|
return ErrBigArgument
|
|
|
|
}
|
|
|
|
|
|
|
|
if !ic.VM.AddGas(gas.Int64()) {
|
|
|
|
return ErrNotEnoughGas
|
|
|
|
}
|
|
|
|
o.GAS.mint(ic, o.Hash, gas)
|
|
|
|
si := ic.DAO.GetStorageItem(o.ContractID, prefixRequestID)
|
|
|
|
id := binary.LittleEndian.Uint64(si.Value) + 1
|
|
|
|
binary.LittleEndian.PutUint64(si.Value, id)
|
|
|
|
if err := ic.DAO.PutStorageItem(o.ContractID, prefixRequestID, si); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Should be executed from contract.
|
|
|
|
_, err := ic.DAO.GetContractState(ic.VM.GetCallingScriptHash())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
w := io.NewBufBinWriter()
|
|
|
|
stackitem.EncodeBinaryStackItem(userData, w.BinWriter)
|
|
|
|
if w.Err != nil {
|
|
|
|
return w.Err
|
|
|
|
}
|
|
|
|
data := w.Bytes()
|
|
|
|
if len(data) > maxUserDataLength {
|
|
|
|
return ErrBigArgument
|
|
|
|
}
|
|
|
|
|
2020-11-05 19:51:08 +00:00
|
|
|
ic.Notifications = append(ic.Notifications, state.NotificationEvent{
|
|
|
|
ScriptHash: o.Hash,
|
|
|
|
Name: "OracleRequest",
|
|
|
|
Item: stackitem.NewArray([]stackitem.Item{
|
|
|
|
stackitem.Make(id),
|
|
|
|
stackitem.Make(ic.VM.GetCallingScriptHash().BytesBE()),
|
|
|
|
stackitem.Make(url),
|
|
|
|
stackitem.Make(filter),
|
|
|
|
}),
|
|
|
|
})
|
2020-10-07 12:06:10 +00:00
|
|
|
req := &state.OracleRequest{
|
2020-09-18 13:26:36 +00:00
|
|
|
OriginalTxID: o.getOriginalTxID(ic.DAO, ic.Tx),
|
|
|
|
GasForResponse: gas.Uint64(),
|
|
|
|
URL: url,
|
|
|
|
Filter: &filter,
|
|
|
|
CallbackContract: ic.VM.GetCallingScriptHash(),
|
|
|
|
CallbackMethod: cb,
|
|
|
|
UserData: data,
|
|
|
|
}
|
2020-09-24 13:33:40 +00:00
|
|
|
return o.PutRequestInternal(id, req, ic.DAO)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PutRequestInternal puts oracle request with the specified id to d.
|
2020-10-07 12:06:10 +00:00
|
|
|
func (o *Oracle) PutRequestInternal(id uint64, req *state.OracleRequest, d dao.DAO) error {
|
2020-09-18 13:26:36 +00:00
|
|
|
reqItem := &state.StorageItem{Value: req.Bytes()}
|
|
|
|
reqKey := makeRequestKey(id)
|
2020-09-24 13:33:40 +00:00
|
|
|
if err := d.PutStorageItem(o.ContractID, reqKey, reqItem); err != nil {
|
2020-09-18 13:26:36 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add request ID to the id list.
|
|
|
|
lst := new(IDList)
|
2020-09-24 13:33:40 +00:00
|
|
|
key := makeIDListKey(req.URL)
|
|
|
|
if err := o.getSerializableFromDAO(d, key, lst); err != nil && !errors.Is(err, storage.ErrKeyNotFound) {
|
2020-09-18 13:26:36 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-10-21 10:02:46 +00:00
|
|
|
if len(*lst) >= maxRequestsCount {
|
|
|
|
return fmt.Errorf("there are too many pending requests for %s url", req.URL)
|
|
|
|
}
|
2020-09-18 13:26:36 +00:00
|
|
|
*lst = append(*lst, id)
|
2020-09-24 13:33:40 +00:00
|
|
|
si := &state.StorageItem{Value: lst.Bytes()}
|
|
|
|
return d.PutStorageItem(o.ContractID, key, si)
|
2020-09-18 13:26:36 +00:00
|
|
|
}
|
|
|
|
|
2020-09-24 12:50:52 +00:00
|
|
|
// GetScriptHash returns script hash or oracle nodes.
|
2020-11-05 16:34:48 +00:00
|
|
|
func (o *Oracle) GetScriptHash(d dao.DAO) (util.Uint160, error) {
|
|
|
|
return o.Desig.getLastDesignatedHash(d, RoleOracle)
|
2020-09-24 12:50:52 +00:00
|
|
|
}
|
|
|
|
|
2020-10-01 15:17:09 +00:00
|
|
|
// GetOracleNodes returns public keys of oracle nodes.
|
|
|
|
func (o *Oracle) GetOracleNodes(d dao.DAO) (keys.PublicKeys, error) {
|
2020-11-05 16:34:48 +00:00
|
|
|
nodes, _, err := o.Desig.GetDesignatedByRole(d, RoleOracle, math.MaxUint32)
|
|
|
|
return nodes, err
|
2020-09-18 13:26:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// GetRequestInternal returns request by ID and key under which it is stored.
|
2020-10-07 12:06:10 +00:00
|
|
|
func (o *Oracle) GetRequestInternal(d dao.DAO, id uint64) (*state.OracleRequest, error) {
|
2020-09-18 13:26:36 +00:00
|
|
|
key := makeRequestKey(id)
|
2020-10-07 12:06:10 +00:00
|
|
|
req := new(state.OracleRequest)
|
2020-09-18 13:26:36 +00:00
|
|
|
return req, o.getSerializableFromDAO(d, key, req)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetIDListInternal returns request by ID and key under which it is stored.
|
|
|
|
func (o *Oracle) GetIDListInternal(d dao.DAO, url string) (*IDList, error) {
|
|
|
|
key := makeIDListKey(url)
|
|
|
|
idList := new(IDList)
|
|
|
|
return idList, o.getSerializableFromDAO(d, key, idList)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Oracle) verify(ic *interop.Context, _ []stackitem.Item) stackitem.Item {
|
|
|
|
return stackitem.NewBool(ic.Tx.HasAttribute(transaction.OracleResponseT))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Oracle) getOriginalTxID(d dao.DAO, tx *transaction.Transaction) util.Uint256 {
|
|
|
|
for i := range tx.Attributes {
|
|
|
|
if tx.Attributes[i].Type == transaction.OracleResponseT {
|
|
|
|
id := tx.Attributes[i].Value.(*transaction.OracleResponse).ID
|
|
|
|
req, _ := o.GetRequestInternal(d, id)
|
|
|
|
return req.OriginalTxID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return tx.Hash()
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeRequestKey(id uint64) []byte {
|
|
|
|
k := make([]byte, 9)
|
|
|
|
k[0] = prefixRequest[0]
|
|
|
|
binary.LittleEndian.PutUint64(k[1:], id)
|
|
|
|
return k
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeIDListKey(url string) []byte {
|
|
|
|
return append(prefixIDList, hash.Hash160([]byte(url)).BytesBE()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *Oracle) getSerializableFromDAO(d dao.DAO, key []byte, item io.Serializable) error {
|
2020-10-01 15:17:09 +00:00
|
|
|
return getSerializableFromDAO(o.ContractID, d, key, item)
|
2020-09-24 12:46:31 +00:00
|
|
|
}
|