forked from TrueCloudLab/neoneo-go
core: move contractCall* to a separate package
This commit is contained in:
parent
4b0008708b
commit
877b8ece63
5 changed files with 103 additions and 90 deletions
89
pkg/core/interop/contract/call.go
Normal file
89
pkg/core/interop/contract/call.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package contract
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
|
"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/stackitem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call calls a contract.
|
||||||
|
func Call(ic *interop.Context) error {
|
||||||
|
h := ic.VM.Estack().Pop().Bytes()
|
||||||
|
method := ic.VM.Estack().Pop().String()
|
||||||
|
args := ic.VM.Estack().Pop().Array()
|
||||||
|
return callExInternal(ic, h, method, args, smartcontract.All)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallEx calls a contract with flags.
|
||||||
|
func CallEx(ic *interop.Context) error {
|
||||||
|
h := ic.VM.Estack().Pop().Bytes()
|
||||||
|
method := ic.VM.Estack().Pop().String()
|
||||||
|
args := ic.VM.Estack().Pop().Array()
|
||||||
|
flags := smartcontract.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64()))
|
||||||
|
if flags&^smartcontract.All != 0 {
|
||||||
|
return errors.New("call flags out of range")
|
||||||
|
}
|
||||||
|
return callExInternal(ic, h, method, args, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func callExInternal(ic *interop.Context, h []byte, name string, args []stackitem.Item, f smartcontract.CallFlag) error {
|
||||||
|
u, err := util.Uint160DecodeBytesBE(h)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid contract hash")
|
||||||
|
}
|
||||||
|
cs, err := ic.DAO.GetContractState(u)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("contract not found")
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(name, "_") {
|
||||||
|
return errors.New("invalid method name (starts with '_')")
|
||||||
|
}
|
||||||
|
md := cs.Manifest.ABI.GetMethod(name)
|
||||||
|
if md == nil {
|
||||||
|
return fmt.Errorf("method '%s' not found", name)
|
||||||
|
}
|
||||||
|
curr, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash())
|
||||||
|
if err == nil {
|
||||||
|
if !curr.Manifest.CanCall(&cs.Manifest, name) {
|
||||||
|
return errors.New("disallowed method call")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) != len(md.Parameters) {
|
||||||
|
return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters))
|
||||||
|
}
|
||||||
|
|
||||||
|
ic.Invocations[u]++
|
||||||
|
ic.VM.LoadScriptWithHash(cs.Script, u, ic.VM.Context().GetCallFlags()&f)
|
||||||
|
var isNative bool
|
||||||
|
for i := range ic.Natives {
|
||||||
|
if ic.Natives[i].Metadata().Hash.Equals(u) {
|
||||||
|
isNative = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isNative {
|
||||||
|
ic.VM.Estack().PushVal(args)
|
||||||
|
ic.VM.Estack().PushVal(name)
|
||||||
|
} else {
|
||||||
|
for i := len(args) - 1; i >= 0; i-- {
|
||||||
|
ic.VM.Estack().PushVal(args[i])
|
||||||
|
}
|
||||||
|
// use Jump not Call here because context was loaded in LoadScript above.
|
||||||
|
ic.VM.Jump(ic.VM.Context(), md.Offset)
|
||||||
|
ic.VM.Context().CheckReturn = true
|
||||||
|
}
|
||||||
|
|
||||||
|
md = cs.Manifest.ABI.GetMethod(manifest.MethodInit)
|
||||||
|
if md != nil {
|
||||||
|
ic.VM.Call(ic.VM.Context(), md.Offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
||||||
|
@ -15,8 +14,6 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"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/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
@ -454,82 +451,6 @@ func storageContextAsReadOnly(ic *interop.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// contractCall calls a contract.
|
|
||||||
func contractCall(ic *interop.Context) error {
|
|
||||||
h := ic.VM.Estack().Pop().Bytes()
|
|
||||||
method := ic.VM.Estack().Pop().String()
|
|
||||||
args := ic.VM.Estack().Pop().Array()
|
|
||||||
return contractCallExInternal(ic, h, method, args, smartcontract.All)
|
|
||||||
}
|
|
||||||
|
|
||||||
// contractCallEx calls a contract with flags.
|
|
||||||
func contractCallEx(ic *interop.Context) error {
|
|
||||||
h := ic.VM.Estack().Pop().Bytes()
|
|
||||||
method := ic.VM.Estack().Pop().String()
|
|
||||||
args := ic.VM.Estack().Pop().Array()
|
|
||||||
flags := smartcontract.CallFlag(int32(ic.VM.Estack().Pop().BigInt().Int64()))
|
|
||||||
if flags&^smartcontract.All != 0 {
|
|
||||||
return errors.New("call flags out of range")
|
|
||||||
}
|
|
||||||
return contractCallExInternal(ic, h, method, args, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
func contractCallExInternal(ic *interop.Context, h []byte, name string, args []stackitem.Item, f smartcontract.CallFlag) error {
|
|
||||||
u, err := util.Uint160DecodeBytesBE(h)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("invalid contract hash")
|
|
||||||
}
|
|
||||||
cs, err := ic.DAO.GetContractState(u)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("contract not found")
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(name, "_") {
|
|
||||||
return errors.New("invalid method name (starts with '_')")
|
|
||||||
}
|
|
||||||
md := cs.Manifest.ABI.GetMethod(name)
|
|
||||||
if md == nil {
|
|
||||||
return fmt.Errorf("method '%s' not found", name)
|
|
||||||
}
|
|
||||||
curr, err := ic.DAO.GetContractState(ic.VM.GetCurrentScriptHash())
|
|
||||||
if err == nil {
|
|
||||||
if !curr.Manifest.CanCall(&cs.Manifest, name) {
|
|
||||||
return errors.New("disallowed method call")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(args) != len(md.Parameters) {
|
|
||||||
return fmt.Errorf("invalid argument count: %d (expected %d)", len(args), len(md.Parameters))
|
|
||||||
}
|
|
||||||
|
|
||||||
ic.Invocations[u]++
|
|
||||||
ic.VM.LoadScriptWithHash(cs.Script, u, ic.VM.Context().GetCallFlags()&f)
|
|
||||||
var isNative bool
|
|
||||||
for i := range ic.Natives {
|
|
||||||
if ic.Natives[i].Metadata().Hash.Equals(u) {
|
|
||||||
isNative = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if isNative {
|
|
||||||
ic.VM.Estack().PushVal(args)
|
|
||||||
ic.VM.Estack().PushVal(name)
|
|
||||||
} else {
|
|
||||||
for i := len(args) - 1; i >= 0; i-- {
|
|
||||||
ic.VM.Estack().PushVal(args[i])
|
|
||||||
}
|
|
||||||
// use Jump not Call here because context was loaded in LoadScript above.
|
|
||||||
ic.VM.Jump(ic.VM.Context(), md.Offset)
|
|
||||||
ic.VM.Context().CheckReturn = true
|
|
||||||
}
|
|
||||||
|
|
||||||
md = cs.Manifest.ABI.GetMethod(manifest.MethodInit)
|
|
||||||
if md != nil {
|
|
||||||
ic.VM.Call(ic.VM.Context(), md.Offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// contractDestroy destroys a contract.
|
// contractDestroy destroys a contract.
|
||||||
func contractDestroy(ic *interop.Context) error {
|
func contractDestroy(ic *interop.Context) error {
|
||||||
hash := ic.VM.GetCurrentScriptHash()
|
hash := ic.VM.GetCurrentScriptHash()
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/runtime"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
@ -441,7 +442,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(addArgs)
|
ic.VM.Estack().PushVal(addArgs)
|
||||||
ic.VM.Estack().PushVal("add")
|
ic.VM.Estack().PushVal("add")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
require.Equal(t, 2, ic.VM.Estack().Len())
|
require.Equal(t, 2, ic.VM.Estack().Len())
|
||||||
require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value())
|
require.Equal(t, big.NewInt(3), ic.VM.Estack().Pop().Value())
|
||||||
|
@ -454,7 +455,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(addArgs)
|
ic.VM.Estack().PushVal(addArgs)
|
||||||
ic.VM.Estack().PushVal("add")
|
ic.VM.Estack().PushVal("add")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.Error(t, contractCallEx(ic))
|
require.Error(t, contract.CallEx(ic))
|
||||||
})
|
})
|
||||||
|
|
||||||
runInvalid := func(args ...interface{}) func(t *testing.T) {
|
runInvalid := func(args ...interface{}) func(t *testing.T) {
|
||||||
|
@ -466,7 +467,7 @@ func TestContractCall(t *testing.T) {
|
||||||
// interops can both return error and panic,
|
// interops can both return error and panic,
|
||||||
// we don't care which kind of error has occurred
|
// we don't care which kind of error has occurred
|
||||||
require.Panics(t, func() {
|
require.Panics(t, func() {
|
||||||
err := contractCall(ic)
|
err := contract.Call(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -491,7 +492,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
||||||
ic.VM.Estack().PushVal("invalidReturn")
|
ic.VM.Estack().PushVal("invalidReturn")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.Error(t, ic.VM.Run())
|
require.Error(t, ic.VM.Run())
|
||||||
})
|
})
|
||||||
t.Run("Void", func(t *testing.T) {
|
t.Run("Void", func(t *testing.T) {
|
||||||
|
@ -499,7 +500,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
||||||
ic.VM.Estack().PushVal("justReturn")
|
ic.VM.Estack().PushVal("justReturn")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
require.Equal(t, 2, ic.VM.Estack().Len())
|
require.Equal(t, 2, ic.VM.Estack().Len())
|
||||||
require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item())
|
require.Equal(t, stackitem.Null{}, ic.VM.Estack().Pop().Item())
|
||||||
|
@ -512,7 +513,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
ic.VM.Estack().PushVal(stackitem.NewArray(nil))
|
||||||
ic.VM.Estack().PushVal("drop")
|
ic.VM.Estack().PushVal("drop")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.Error(t, ic.VM.Run())
|
require.Error(t, ic.VM.Run())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -523,7 +524,7 @@ func TestContractCall(t *testing.T) {
|
||||||
ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)}))
|
ic.VM.Estack().PushVal(stackitem.NewArray([]stackitem.Item{stackitem.Make(5)}))
|
||||||
ic.VM.Estack().PushVal("add3")
|
ic.VM.Estack().PushVal("add3")
|
||||||
ic.VM.Estack().PushVal(h.BytesBE())
|
ic.VM.Estack().PushVal(h.BytesBE())
|
||||||
require.NoError(t, contractCall(ic))
|
require.NoError(t, contract.Call(ic))
|
||||||
require.NoError(t, ic.VM.Run())
|
require.NoError(t, ic.VM.Run())
|
||||||
require.Equal(t, 2, ic.VM.Estack().Len())
|
require.Equal(t, 2, ic.VM.Estack().Len())
|
||||||
require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value())
|
require.Equal(t, big.NewInt(8), ic.VM.Estack().Pop().Value())
|
||||||
|
|
|
@ -10,6 +10,7 @@ package core
|
||||||
import (
|
import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/callback"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/crypto"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/crypto"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/enumerator"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames"
|
||||||
|
@ -53,9 +54,9 @@ var systemInterops = []interop.Function{
|
||||||
{Name: interopnames.SystemCallbackCreateFromMethod, Func: callback.CreateFromMethod, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
{Name: interopnames.SystemCallbackCreateFromMethod, Func: callback.CreateFromMethod, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemCallbackCreateFromSyscall, Func: callback.CreateFromSyscall, Price: 400, ParamCount: 1, DisallowCallback: true},
|
{Name: interopnames.SystemCallbackCreateFromSyscall, Func: callback.CreateFromSyscall, Price: 400, ParamCount: 1, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemCallbackInvoke, Func: callback.Invoke, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
{Name: interopnames.SystemCallbackInvoke, Func: callback.Invoke, Price: 1000000, ParamCount: 2, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemContractCall, Func: contractCall, Price: 1000000,
|
{Name: interopnames.SystemContractCall, Func: contract.Call, Price: 1000000,
|
||||||
RequiredFlags: smartcontract.AllowCall, ParamCount: 3, DisallowCallback: true},
|
RequiredFlags: smartcontract.AllowCall, ParamCount: 3, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemContractCallEx, Func: contractCallEx, Price: 1000000,
|
{Name: interopnames.SystemContractCallEx, Func: contract.CallEx, Price: 1000000,
|
||||||
RequiredFlags: smartcontract.AllowCall, ParamCount: 4, DisallowCallback: true},
|
RequiredFlags: smartcontract.AllowCall, ParamCount: 4, DisallowCallback: true},
|
||||||
{Name: interopnames.SystemContractCreate, Func: contractCreate, Price: 0,
|
{Name: interopnames.SystemContractCreate, Func: contractCreate, Price: 0,
|
||||||
RequiredFlags: smartcontract.AllowModifyStates, ParamCount: 2, DisallowCallback: true},
|
RequiredFlags: smartcontract.AllowModifyStates, ParamCount: 2, DisallowCallback: true},
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/dao"
|
"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"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/interop/contract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"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/storage"
|
||||||
|
@ -131,7 +132,7 @@ func (tn *testNative) callOtherContractWithoutArgs(ic *interop.Context, args []s
|
||||||
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{})) // no args
|
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{})) // no args
|
||||||
vm.Estack().PushVal(args[1]) // method
|
vm.Estack().PushVal(args[1]) // method
|
||||||
vm.Estack().PushVal(args[0]) // contract hash
|
vm.Estack().PushVal(args[0]) // contract hash
|
||||||
err := contractCall(ic)
|
err := contract.Call(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stackitem.NewBigInteger(big.NewInt(-1))
|
return stackitem.NewBigInteger(big.NewInt(-1))
|
||||||
}
|
}
|
||||||
|
@ -147,7 +148,7 @@ func (tn *testNative) callOtherContractWithArg(ic *interop.Context, args []stack
|
||||||
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{args[2]})) // arg
|
vm.Estack().PushVal(stackitem.NewArray([]stackitem.Item{args[2]})) // arg
|
||||||
vm.Estack().PushVal(args[1]) // method
|
vm.Estack().PushVal(args[1]) // method
|
||||||
vm.Estack().PushVal(args[0]) // contract hash
|
vm.Estack().PushVal(args[0]) // contract hash
|
||||||
err := contractCall(ic)
|
err := contract.Call(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return stackitem.NewBigInteger(big.NewInt(-1))
|
return stackitem.NewBigInteger(big.NewInt(-1))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue